Skip to content

Commit

Permalink
Add Schema.max_query_strings_tokens limit
Browse files Browse the repository at this point in the history
  • Loading branch information
rmosolgo committed Apr 22, 2024
1 parent dc62848 commit c634445
Show file tree
Hide file tree
Showing 9 changed files with 56 additions and 8 deletions.
3 changes: 3 additions & 0 deletions lib/generators/graphql/templates/schema.erb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ class <%= schema_name %> < GraphQL::Schema
raise(GraphQL::RequiredImplementationMissingError)
end

# Limit the size of incoming queries:
max_query_string_tokens(5000)

# Stop validating when it encounters this many errors:
validate_max_errors(100)
end
Expand Down
4 changes: 2 additions & 2 deletions lib/graphql.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ def default_parser
# Turn a query string or schema definition into an AST
# @param graphql_string [String] a GraphQL query string or schema definition
# @return [GraphQL::Language::Nodes::Document]
def self.parse(graphql_string, trace: GraphQL::Tracing::NullTrace, filename: nil)
default_parser.parse(graphql_string, trace: trace, filename: filename)
def self.parse(graphql_string, trace: GraphQL::Tracing::NullTrace, filename: nil, max_tokens: nil)
default_parser.parse(graphql_string, trace: trace, filename: filename, max_tokens: max_tokens)
end

# Read the contents of `filename` and parse them as GraphQL
Expand Down
8 changes: 7 additions & 1 deletion lib/graphql/language/lexer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,16 @@ module GraphQL
module Language

class Lexer
def initialize(graphql_str, filename: nil)
def initialize(graphql_str, filename: nil, max_tokens: Float::INFINITY)
if !(graphql_str.encoding == Encoding::UTF_8 || graphql_str.ascii_only?)
graphql_str = graphql_str.dup.force_encoding(Encoding::UTF_8)
end
@string = graphql_str
@filename = filename
@scanner = StringScanner.new(graphql_str)
@pos = nil
@max_tokens = max_tokens
@tokens_count = 0
end

def eos?
Expand All @@ -22,6 +24,10 @@ def eos?
def advance
@scanner.skip(IGNORE_REGEXP)
return false if @scanner.eos?
@tokens_count += 1
if @tokens_count > @max_tokens
raise_parse_error("This query is too large to execute.")
end
@pos = @scanner.pos
next_byte = @string.getbyte(@pos)
next_byte_is_for = FIRST_BYTES[next_byte]
Expand Down
8 changes: 4 additions & 4 deletions lib/graphql/language/parser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ class Parser
class << self
attr_accessor :cache

def parse(graphql_str, filename: nil, trace: Tracing::NullTrace)
self.new(graphql_str, filename: filename, trace: trace).parse
def parse(graphql_str, filename: nil, trace: Tracing::NullTrace, max_tokens: nil)
self.new(graphql_str, filename: filename, trace: trace, max_tokens: max_tokens).parse
end

def parse_file(filename, trace: Tracing::NullTrace)
Expand All @@ -27,11 +27,11 @@ def parse_file(filename, trace: Tracing::NullTrace)
end
end

def initialize(graphql_str, filename: nil, trace: Tracing::NullTrace)
def initialize(graphql_str, filename: nil, trace: Tracing::NullTrace, max_tokens: nil)
if graphql_str.nil?
raise GraphQL::ParseError.new("No query string was present", nil, nil, nil)
end
@lexer = Lexer.new(graphql_str, filename: filename)
@lexer = Lexer.new(graphql_str, filename: filename, max_tokens: max_tokens)
@graphql_str = graphql_str
@filename = filename
@trace = trace
Expand Down
2 changes: 1 addition & 1 deletion lib/graphql/query.rb
Original file line number Diff line number Diff line change
Expand Up @@ -395,7 +395,7 @@ def prepare_ast
parse_error = nil
@document ||= begin
if query_string
GraphQL.parse(query_string, trace: self.current_trace)
GraphQL.parse(query_string, trace: self.current_trace, max_tokens: @schema.max_query_string_tokens)
end
rescue GraphQL::ParseError => err
parse_error = err
Expand Down
11 changes: 11 additions & 0 deletions lib/graphql/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,17 @@ def default_max_page_size(new_default_max_page_size = nil)
end
end

# A limit on the number of tokens to accept on incoming query strings.
# Use this to prevent parsing maliciously-large query strings.
# @return [nil, Integer]
def max_query_string_tokens(new_max_tokens = NOT_CONFIGURED)
if NOT_CONFIGURED.equal?(new_max_tokens)
@max_query_string_tokens || find_inherited_value(:max_query_string_tokens)
else
@max_query_string_tokens = new_max_tokens
end
end

def default_page_size(new_default_page_size = nil)
if new_default_page_size
@default_page_size = new_default_page_size
Expand Down
18 changes: 18 additions & 0 deletions spec/graphql/language/lexer_examples.rb
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,24 @@ def self.included(child_mod)
assert_equal 21, type_keyword_tok_3.line
assert_equal 21, c_name_tok.line
end

it "halts after max_tokens" do
query_type = Class.new(GraphQL::Schema::Object) do
graphql_name "Query"
field :x, Integer
end
schema = Class.new(GraphQL::Schema) do
query(query_type)
max_query_string_tokens(5000)
end

assert_equal 5000, schema.max_query_string_tokens

query_str = "{ x } " * 5000
assert_equal 15000, subject.tokenize(query_str).size
result = schema.execute(query_str)
assert_equal ["This query is too large to execute."], result["errors"].map { |e| e["message"] }
end
end
end
end
Expand Down
4 changes: 4 additions & 0 deletions spec/graphql/schema_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class CustomSubscriptions < GraphQL::Subscriptions::ActionCableSubscriptions
query_analyzer Object.new
multiplex_analyzer Object.new
validate_timeout 100
max_query_string_tokens 500
rescue_from(StandardError) { }
use GraphQL::Backtrace
use GraphQL::Subscriptions::ActionCableSubscriptions, action_cable: nil, action_cable_coder: JSON
Expand All @@ -69,6 +70,7 @@ class CustomSubscriptions < GraphQL::Subscriptions::ActionCableSubscriptions
assert_equal base_schema.orphan_types, schema.orphan_types
assert_equal base_schema.context_class, schema.context_class
assert_equal base_schema.directives, schema.directives
assert_equal base_schema.max_query_string_tokens, schema.max_query_string_tokens
assert_equal base_schema.query_analyzers, schema.query_analyzers
assert_equal base_schema.multiplex_analyzers, schema.multiplex_analyzers
assert_equal base_schema.disable_introspection_entry_points?, schema.disable_introspection_entry_points?
Expand All @@ -86,6 +88,7 @@ class CustomSubscriptions < GraphQL::Subscriptions::ActionCableSubscriptions
use CustomSubscriptions, action_cable: nil, action_cable_coder: JSON
query_class(custom_query_class)
extra_types [extra_type_2]
max_query_string_tokens nil
end

query = Class.new(GraphQL::Schema::Object) do
Expand Down Expand Up @@ -128,6 +131,7 @@ class CustomSubscriptions < GraphQL::Subscriptions::ActionCableSubscriptions
assert_equal subscription, schema.subscription
assert_equal introspection, schema.introspection
assert_equal cursor_encoder, schema.cursor_encoder
assert_nil schema.max_query_string_tokens

assert_equal context_class, schema.context_class
assert_equal 10, schema.validate_timeout
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ def self.resolve_type(abstract_type, obj, ctx)
raise(GraphQL::RequiredImplementationMissingError)
end
# Limit the size of incoming queries:
max_query_string_tokens(5000)
# Stop validating when it encounters this many errors:
validate_max_errors(100)
end
Expand Down Expand Up @@ -378,6 +381,9 @@ def self.resolve_type(abstract_type, obj, ctx)
raise(GraphQL::RequiredImplementationMissingError)
end
# Limit the size of incoming queries:
max_query_string_tokens(5000)
# Stop validating when it encounters this many errors:
validate_max_errors(100)
Expand Down

0 comments on commit c634445

Please sign in to comment.