Skip to content

Commit 370b048

Browse files
authored
Merge pull request #676 from code0-tech/copilot/prevent-recursive-queries
Add max_depth limit to prevent recursive GraphQL queries
2 parents 1308d85 + d4d886e commit 370b048

File tree

2 files changed

+90
-3
lines changed

2 files changed

+90
-3
lines changed

app/graphql/sagittarius_schema.rb

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
# frozen_string_literal: true
22

33
# rubocop:disable GraphQL/MaxComplexitySchema
4-
# rubocop:disable GraphQL/MaxDepthSchema
54
class SagittariusSchema < GraphQL::Schema
65
mutation(Types::MutationType)
76
query(Types::QueryType)
87

98
default_max_page_size 50
9+
max_depth 20
1010
connections.add(ActiveRecord::Relation, Sagittarius::Graphql::StableConnection)
1111

1212
# For batch-loading (see https://graphql-ruby.org/dataloader/overview.html)
1313
use GraphQL::Dataloader
1414

1515
use GraphQL::Schema::AlwaysVisible
1616

17+
# rubocop:enable GraphQL/MaxComplexitySchema
1718
# rubocop:disable Lint/UselessMethodDefinition
1819
# GraphQL-Ruby calls this when something goes wrong while running a query:
1920
def self.type_error(err, context)
@@ -54,8 +55,6 @@ def self.object_from_id(global_id, query_ctx = nil)
5455

5556
# rubocop:enable Lint/UnusedMethodArgument
5657
end
57-
# rubocop:enable GraphQL/MaxDepthSchema
58-
# rubocop:enable GraphQL/MaxComplexitySchema
5958

6059
if Types::BaseObject.instance_variable_defined?(:@user_ability_types)
6160
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 = 25
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 ~7-8)
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)