Skip to content

Commit 8a0dd14

Browse files
CopilotKnerio
andcommitted
Add max_depth protection to GraphQL schema to prevent recursive queries
Co-authored-by: Knerio <96529060+Knerio@users.noreply.github.com>
1 parent d0af63c commit 8a0dd14

File tree

2 files changed

+89
-4
lines changed

2 files changed

+89
-4
lines changed

app/graphql/sagittarius_schema.rb

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
# frozen_string_literal: true
22

3-
# rubocop:disable GraphQL/MaxComplexitySchema
4-
# rubocop:disable GraphQL/MaxDepthSchema
53
class SagittariusSchema < GraphQL::Schema
64
mutation(Types::MutationType)
75
query(Types::QueryType)
86

97
default_max_page_size 50
8+
max_depth 15
109
connections.add(ActiveRecord::Relation, Sagittarius::Graphql::StableConnection)
1110

1211
# For batch-loading (see https://graphql-ruby.org/dataloader/overview.html)
@@ -54,8 +53,6 @@ def self.object_from_id(global_id, query_ctx = nil)
5453

5554
# rubocop:enable Lint/UnusedMethodArgument
5655
end
57-
# rubocop:enable GraphQL/MaxDepthSchema
58-
# rubocop:enable GraphQL/MaxComplexitySchema
5956

6057
if Types::BaseObject.instance_variable_defined?(:@user_ability_types)
6158
Types::BaseObject.remove_instance_variable(:@user_ability_types) # release temporary type map
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# frozen_string_literal: true
2+
3+
require 'rails_helper'
4+
5+
RSpec.describe 'Recursive Query Protection' do
6+
include GraphqlHelpers
7+
8+
let(:current_user) { create(:user) }
9+
let(:organization) { create(:organization) }
10+
11+
before do
12+
create(:namespace_member, namespace: organization.ensure_namespace, user: current_user)
13+
end
14+
15+
context 'with deeply nested recursive query' do
16+
let(:query) do
17+
# Create a query with deep nesting that would cause recursion
18+
# Organization -> Namespace -> Parent (Organization) -> Namespace -> Parent...
19+
nested_levels = 20
20+
nested_query = 'id name'
21+
22+
nested_levels.times do
23+
nested_query = <<~NESTED
24+
id
25+
name
26+
namespace {
27+
id
28+
parent {
29+
... on Organization {
30+
#{nested_query}
31+
}
32+
}
33+
}
34+
NESTED
35+
end
36+
37+
<<~QUERY
38+
query($organizationId: OrganizationID!) {
39+
organization(id: $organizationId) {
40+
#{nested_query}
41+
}
42+
}
43+
QUERY
44+
end
45+
46+
let(:variables) { { organizationId: organization.to_global_id.to_s } }
47+
48+
it 'blocks the query and returns an error' do
49+
post_graphql query, variables: variables, current_user: current_user
50+
51+
expect(graphql_errors).not_to be_nil
52+
expect(graphql_errors).not_to be_empty
53+
expect(graphql_errors.first['message']).to include('depth')
54+
end
55+
end
56+
57+
context 'with reasonably nested query' do
58+
let(:query) do
59+
# A reasonable query with moderate nesting (depth ~5)
60+
<<~QUERY
61+
query($organizationId: OrganizationID!) {
62+
organization(id: $organizationId) {
63+
id
64+
name
65+
namespace {
66+
id
67+
parent {
68+
... on Organization {
69+
id
70+
name
71+
}
72+
}
73+
}
74+
}
75+
}
76+
QUERY
77+
end
78+
79+
let(:variables) { { organizationId: organization.to_global_id.to_s } }
80+
81+
it 'allows the query to execute' do
82+
post_graphql query, variables: variables, current_user: current_user
83+
84+
expect(graphql_data_at(:organization)).not_to be_nil
85+
expect(graphql_data_at(:organization, :id)).to eq(organization.to_global_id.to_s)
86+
end
87+
end
88+
end

0 commit comments

Comments
 (0)