From d0af63ce6474c85781b7fc12cefbee55c335e3eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Nov 2025 21:19:33 +0000 Subject: [PATCH 1/5] Initial plan From 8a0dd14395f4137e830931d760d7d76fa5aaddc6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Nov 2025 21:25:58 +0000 Subject: [PATCH 2/5] Add max_depth protection to GraphQL schema to prevent recursive queries Co-authored-by: Knerio <96529060+Knerio@users.noreply.github.com> --- app/graphql/sagittarius_schema.rb | 5 +- .../graphql/query/recursive_query_spec.rb | 88 +++++++++++++++++++ 2 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 spec/requests/graphql/query/recursive_query_spec.rb diff --git a/app/graphql/sagittarius_schema.rb b/app/graphql/sagittarius_schema.rb index 96ba2a26..f527d7ec 100644 --- a/app/graphql/sagittarius_schema.rb +++ b/app/graphql/sagittarius_schema.rb @@ -1,12 +1,11 @@ # frozen_string_literal: true -# rubocop:disable GraphQL/MaxComplexitySchema -# rubocop:disable GraphQL/MaxDepthSchema class SagittariusSchema < GraphQL::Schema mutation(Types::MutationType) query(Types::QueryType) default_max_page_size 50 + max_depth 15 connections.add(ActiveRecord::Relation, Sagittarius::Graphql::StableConnection) # 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) # rubocop:enable Lint/UnusedMethodArgument end -# rubocop:enable GraphQL/MaxDepthSchema -# rubocop:enable GraphQL/MaxComplexitySchema if Types::BaseObject.instance_variable_defined?(:@user_ability_types) Types::BaseObject.remove_instance_variable(:@user_ability_types) # release temporary type map diff --git a/spec/requests/graphql/query/recursive_query_spec.rb b/spec/requests/graphql/query/recursive_query_spec.rb new file mode 100644 index 00000000..3bc6e292 --- /dev/null +++ b/spec/requests/graphql/query/recursive_query_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Recursive Query Protection' do + include GraphqlHelpers + + let(:current_user) { create(:user) } + let(:organization) { create(:organization) } + + before do + create(:namespace_member, namespace: organization.ensure_namespace, user: current_user) + end + + context 'with deeply nested recursive query' do + let(:query) do + # Create a query with deep nesting that would cause recursion + # Organization -> Namespace -> Parent (Organization) -> Namespace -> Parent... + nested_levels = 20 + nested_query = 'id name' + + nested_levels.times do + nested_query = <<~NESTED + id + name + namespace { + id + parent { + ... on Organization { + #{nested_query} + } + } + } + NESTED + end + + <<~QUERY + query($organizationId: OrganizationID!) { + organization(id: $organizationId) { + #{nested_query} + } + } + QUERY + end + + let(:variables) { { organizationId: organization.to_global_id.to_s } } + + it 'blocks the query and returns an error' do + post_graphql query, variables: variables, current_user: current_user + + expect(graphql_errors).not_to be_nil + expect(graphql_errors).not_to be_empty + expect(graphql_errors.first['message']).to include('depth') + end + end + + context 'with reasonably nested query' do + let(:query) do + # A reasonable query with moderate nesting (depth ~5) + <<~QUERY + query($organizationId: OrganizationID!) { + organization(id: $organizationId) { + id + name + namespace { + id + parent { + ... on Organization { + id + name + } + } + } + } + } + QUERY + end + + let(:variables) { { organizationId: organization.to_global_id.to_s } } + + it 'allows the query to execute' do + post_graphql query, variables: variables, current_user: current_user + + expect(graphql_data_at(:organization)).not_to be_nil + expect(graphql_data_at(:organization, :id)).to eq(organization.to_global_id.to_s) + end + end +end From c155cb501bf1c970ffe6746906cb9a08555fb96e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Nov 2025 21:38:49 +0000 Subject: [PATCH 3/5] Increase max_depth from 15 to 20 for more headroom Co-authored-by: Knerio <96529060+Knerio@users.noreply.github.com> --- app/graphql/sagittarius_schema.rb | 2 +- spec/requests/graphql/query/recursive_query_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/graphql/sagittarius_schema.rb b/app/graphql/sagittarius_schema.rb index f527d7ec..b2e86b58 100644 --- a/app/graphql/sagittarius_schema.rb +++ b/app/graphql/sagittarius_schema.rb @@ -5,7 +5,7 @@ class SagittariusSchema < GraphQL::Schema query(Types::QueryType) default_max_page_size 50 - max_depth 15 + max_depth 20 connections.add(ActiveRecord::Relation, Sagittarius::Graphql::StableConnection) # For batch-loading (see https://graphql-ruby.org/dataloader/overview.html) diff --git a/spec/requests/graphql/query/recursive_query_spec.rb b/spec/requests/graphql/query/recursive_query_spec.rb index 3bc6e292..eb9a2e38 100644 --- a/spec/requests/graphql/query/recursive_query_spec.rb +++ b/spec/requests/graphql/query/recursive_query_spec.rb @@ -16,7 +16,7 @@ let(:query) do # Create a query with deep nesting that would cause recursion # Organization -> Namespace -> Parent (Organization) -> Namespace -> Parent... - nested_levels = 20 + nested_levels = 25 nested_query = 'id name' nested_levels.times do From d05307778fae2987d4b89ab38bad9c76a115cc28 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Nov 2025 22:03:18 +0000 Subject: [PATCH 4/5] Add max_complexity 200 to fix rubocop warning Co-authored-by: Knerio <96529060+Knerio@users.noreply.github.com> --- app/graphql/sagittarius_schema.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/graphql/sagittarius_schema.rb b/app/graphql/sagittarius_schema.rb index b2e86b58..e253df3a 100644 --- a/app/graphql/sagittarius_schema.rb +++ b/app/graphql/sagittarius_schema.rb @@ -6,6 +6,7 @@ class SagittariusSchema < GraphQL::Schema default_max_page_size 50 max_depth 20 + max_complexity 200 connections.add(ActiveRecord::Relation, Sagittarius::Graphql::StableConnection) # For batch-loading (see https://graphql-ruby.org/dataloader/overview.html) From d4d886ec9a3b9564ca156da476c9f4cfd427c224 Mon Sep 17 00:00:00 2001 From: Dario Pranjic <96529060+Knerio@users.noreply.github.com> Date: Thu, 13 Nov 2025 23:04:13 +0100 Subject: [PATCH 5/5] Update spec/requests/graphql/query/recursive_query_spec.rb Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Signed-off-by: Dario Pranjic <96529060+Knerio@users.noreply.github.com> --- app/graphql/sagittarius_schema.rb | 3 ++- spec/requests/graphql/query/recursive_query_spec.rb | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/graphql/sagittarius_schema.rb b/app/graphql/sagittarius_schema.rb index e253df3a..63579065 100644 --- a/app/graphql/sagittarius_schema.rb +++ b/app/graphql/sagittarius_schema.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true +# rubocop:disable GraphQL/MaxComplexitySchema class SagittariusSchema < GraphQL::Schema mutation(Types::MutationType) query(Types::QueryType) default_max_page_size 50 max_depth 20 - max_complexity 200 connections.add(ActiveRecord::Relation, Sagittarius::Graphql::StableConnection) # For batch-loading (see https://graphql-ruby.org/dataloader/overview.html) @@ -14,6 +14,7 @@ class SagittariusSchema < GraphQL::Schema use GraphQL::Schema::AlwaysVisible + # rubocop:enable GraphQL/MaxComplexitySchema # rubocop:disable Lint/UselessMethodDefinition # GraphQL-Ruby calls this when something goes wrong while running a query: def self.type_error(err, context) diff --git a/spec/requests/graphql/query/recursive_query_spec.rb b/spec/requests/graphql/query/recursive_query_spec.rb index eb9a2e38..542ad190 100644 --- a/spec/requests/graphql/query/recursive_query_spec.rb +++ b/spec/requests/graphql/query/recursive_query_spec.rb @@ -18,7 +18,7 @@ # Organization -> Namespace -> Parent (Organization) -> Namespace -> Parent... nested_levels = 25 nested_query = 'id name' - + nested_levels.times do nested_query = <<~NESTED id @@ -56,7 +56,7 @@ context 'with reasonably nested query' do let(:query) do - # A reasonable query with moderate nesting (depth ~5) + # A reasonable query with moderate nesting (depth ~7-8) <<~QUERY query($organizationId: OrganizationID!) { organization(id: $organizationId) {