diff --git a/app/graphql/sagittarius_schema.rb b/app/graphql/sagittarius_schema.rb index 96ba2a26..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 -# rubocop:disable GraphQL/MaxDepthSchema class SagittariusSchema < GraphQL::Schema mutation(Types::MutationType) query(Types::QueryType) default_max_page_size 50 + max_depth 20 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) @@ -54,8 +55,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..542ad190 --- /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 = 25 + 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 ~7-8) + <<~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