Skip to content

Commit d1117ae

Browse files
authored
Merge commit from fork
[2.1] fix CVE 2025 27407
2 parents 0f1a791 + 243fcdc commit d1117ae

30 files changed

+354
-120
lines changed

.rubocop.yml

+5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require:
22
- ./cop/development/none_without_block_cop
3+
- ./cop/development/no_eval_cop
34
- ./cop/development/no_focus_cop
45
- ./lib/graphql/rubocop/graphql/default_null_true
56
- ./lib/graphql/rubocop/graphql/default_required_true
@@ -51,6 +52,10 @@ Development/NoneWithoutBlockCop:
5152
- "lib/**/*"
5253
- "spec/**/*"
5354

55+
Development/NoEvalCop:
56+
Include:
57+
- "lib/**/*"
58+
5459
Development/NoFocusCop:
5560
Include:
5661
- "spec/**/*"

benchmark/run.rb

+7-2
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,16 @@ def self.build_large_schema
110110
end
111111

112112
obj_ts = 100.times.map do |n|
113+
input_obj_t = Class.new(GraphQL::Schema::InputObject) do
114+
graphql_name("Input#{n}")
115+
argument :arg, String
116+
end
113117
obj_t = Class.new(GraphQL::Schema::Object) do
114118
graphql_name("Object#{n}")
115119
implements(*int_ts)
116120
20.times do |n2|
117121
field :"field#{n2}", String do
118-
argument :arg, String
122+
argument :input, input_obj_t
119123
end
120124

121125
end
@@ -152,8 +156,9 @@ def self.profile_boot
152156
end
153157
StackProf::Report.new(result).print_text
154158

159+
retained_schema = nil
155160
report = MemoryProfiler.report do
156-
build_large_schema
161+
retained_schema = build_large_schema
157162
end
158163

159164
report.pretty_print

cop/development/no_eval_cop.rb

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# frozen_string_literal: true
2+
require 'rubocop'
3+
4+
module Cop
5+
module Development
6+
class NoEvalCop < RuboCop::Cop::Base
7+
MSG_TEMPLATE = "Don't use `%{eval_method_name}` which accepts strings and may result evaluating unexpected code. Use `%{exec_method_name}` instead, and pass a block."
8+
9+
def on_send(node)
10+
case node.method_name
11+
when :module_eval, :class_eval, :instance_eval
12+
message = MSG_TEMPLATE % { eval_method_name: node.method_name, exec_method_name: node.method_name.to_s.sub("eval", "exec").to_sym }
13+
add_offense node, message: message
14+
end
15+
end
16+
end
17+
end
18+
end

lib/graphql/language/nodes.rb

+3
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ def merge!(new_options)
138138
end
139139

140140
class << self
141+
# rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time
142+
141143
# Add a default `#visit_method` and `#children_method_name` using the class name
142144
def inherited(child_class)
143145
super
@@ -296,6 +298,7 @@ def self.from_a(filename, line, col, #{(scalar_method_names + @children_methods.
296298
RUBY
297299
end
298300
end
301+
# rubocop:enable Development/NoEvalCop
299302
end
300303
end
301304

lib/graphql/language/static_visitor.rb

+37-33
Original file line numberDiff line numberDiff line change
@@ -22,39 +22,6 @@ def visit
2222
end
2323
end
2424

25-
# We don't use `alias` here because it breaks `super`
26-
def self.make_visit_methods(ast_node_class)
27-
node_method = ast_node_class.visit_method
28-
children_of_type = ast_node_class.children_of_type
29-
child_visit_method = :"#{node_method}_children"
30-
31-
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
32-
# The default implementation for visiting an AST node.
33-
# It doesn't _do_ anything, but it continues to visiting the node's children.
34-
# To customize this hook, override one of its make_visit_methods (or the base method?)
35-
# in your subclasses.
36-
#
37-
# @param node [GraphQL::Language::Nodes::AbstractNode] the node being visited
38-
# @param parent [GraphQL::Language::Nodes::AbstractNode, nil] the previously-visited node, or `nil` if this is the root node.
39-
# @return [void]
40-
def #{node_method}(node, parent)
41-
#{
42-
if method_defined?(child_visit_method)
43-
"#{child_visit_method}(node)"
44-
elsif children_of_type
45-
children_of_type.map do |child_accessor, child_class|
46-
"node.#{child_accessor}.each do |child_node|
47-
#{child_class.visit_method}(child_node, node)
48-
end"
49-
end.join("\n")
50-
else
51-
""
52-
end
53-
}
54-
end
55-
RUBY
56-
end
57-
5825
def on_document_children(document_node)
5926
document_node.children.each do |child_node|
6027
visit_method = child_node.visit_method
@@ -123,6 +90,41 @@ def on_argument_children(new_node)
12390
end
12491
end
12592

93+
# rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time
94+
95+
# We don't use `alias` here because it breaks `super`
96+
def self.make_visit_methods(ast_node_class)
97+
node_method = ast_node_class.visit_method
98+
children_of_type = ast_node_class.children_of_type
99+
child_visit_method = :"#{node_method}_children"
100+
101+
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
102+
# The default implementation for visiting an AST node.
103+
# It doesn't _do_ anything, but it continues to visiting the node's children.
104+
# To customize this hook, override one of its make_visit_methods (or the base method?)
105+
# in your subclasses.
106+
#
107+
# @param node [GraphQL::Language::Nodes::AbstractNode] the node being visited
108+
# @param parent [GraphQL::Language::Nodes::AbstractNode, nil] the previously-visited node, or `nil` if this is the root node.
109+
# @return [void]
110+
def #{node_method}(node, parent)
111+
#{
112+
if method_defined?(child_visit_method)
113+
"#{child_visit_method}(node)"
114+
elsif children_of_type
115+
children_of_type.map do |child_accessor, child_class|
116+
"node.#{child_accessor}.each do |child_node|
117+
#{child_class.visit_method}(child_node, node)
118+
end"
119+
end.join("\n")
120+
else
121+
""
122+
end
123+
}
124+
end
125+
RUBY
126+
end
127+
126128
[
127129
Language::Nodes::Argument,
128130
Language::Nodes::Directive,
@@ -162,6 +164,8 @@ def on_argument_children(new_node)
162164
].each do |ast_node_class|
163165
make_visit_methods(ast_node_class)
164166
end
167+
168+
# rubocop:disable Development/NoEvalCop
165169
end
166170
end
167171
end

lib/graphql/language/visitor.rb

+59-55
Original file line numberDiff line numberDiff line change
@@ -61,61 +61,6 @@ def visit
6161
end
6262
end
6363

64-
# We don't use `alias` here because it breaks `super`
65-
def self.make_visit_methods(ast_node_class)
66-
node_method = ast_node_class.visit_method
67-
children_of_type = ast_node_class.children_of_type
68-
child_visit_method = :"#{node_method}_children"
69-
70-
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
71-
# The default implementation for visiting an AST node.
72-
# It doesn't _do_ anything, but it continues to visiting the node's children.
73-
# To customize this hook, override one of its make_visit_methods (or the base method?)
74-
# in your subclasses.
75-
#
76-
# @param node [GraphQL::Language::Nodes::AbstractNode] the node being visited
77-
# @param parent [GraphQL::Language::Nodes::AbstractNode, nil] the previously-visited node, or `nil` if this is the root node.
78-
# @return [Array, nil] If there were modifications, it returns an array of new nodes, otherwise, it returns `nil`.
79-
def #{node_method}(node, parent)
80-
if node.equal?(DELETE_NODE)
81-
# This might be passed to `super(DELETE_NODE, ...)`
82-
# by a user hook, don't want to keep visiting in that case.
83-
[node, parent]
84-
else
85-
new_node = node
86-
#{
87-
if method_defined?(child_visit_method)
88-
"new_node = #{child_visit_method}(new_node)"
89-
elsif children_of_type
90-
children_of_type.map do |child_accessor, child_class|
91-
"node.#{child_accessor}.each do |child_node|
92-
new_child_and_node = #{child_class.visit_method}_with_modifications(child_node, new_node)
93-
# Reassign `node` in case the child hook makes a modification
94-
if new_child_and_node.is_a?(Array)
95-
new_node = new_child_and_node[1]
96-
end
97-
end"
98-
end.join("\n")
99-
else
100-
""
101-
end
102-
}
103-
104-
if new_node.equal?(node)
105-
[node, parent]
106-
else
107-
[new_node, parent]
108-
end
109-
end
110-
end
111-
112-
def #{node_method}_with_modifications(node, parent)
113-
new_node_and_new_parent = #{node_method}(node, parent)
114-
apply_modifications(node, parent, new_node_and_new_parent)
115-
end
116-
RUBY
117-
end
118-
11964
def on_document_children(document_node)
12065
new_node = document_node
12166
document_node.children.each do |child_node|
@@ -216,6 +161,63 @@ def on_argument_children(new_node)
216161
new_node
217162
end
218163

164+
# rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time
165+
166+
# We don't use `alias` here because it breaks `super`
167+
def self.make_visit_methods(ast_node_class)
168+
node_method = ast_node_class.visit_method
169+
children_of_type = ast_node_class.children_of_type
170+
child_visit_method = :"#{node_method}_children"
171+
172+
class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
173+
# The default implementation for visiting an AST node.
174+
# It doesn't _do_ anything, but it continues to visiting the node's children.
175+
# To customize this hook, override one of its make_visit_methods (or the base method?)
176+
# in your subclasses.
177+
#
178+
# @param node [GraphQL::Language::Nodes::AbstractNode] the node being visited
179+
# @param parent [GraphQL::Language::Nodes::AbstractNode, nil] the previously-visited node, or `nil` if this is the root node.
180+
# @return [Array, nil] If there were modifications, it returns an array of new nodes, otherwise, it returns `nil`.
181+
def #{node_method}(node, parent)
182+
if node.equal?(DELETE_NODE)
183+
# This might be passed to `super(DELETE_NODE, ...)`
184+
# by a user hook, don't want to keep visiting in that case.
185+
[node, parent]
186+
else
187+
new_node = node
188+
#{
189+
if method_defined?(child_visit_method)
190+
"new_node = #{child_visit_method}(new_node)"
191+
elsif children_of_type
192+
children_of_type.map do |child_accessor, child_class|
193+
"node.#{child_accessor}.each do |child_node|
194+
new_child_and_node = #{child_class.visit_method}_with_modifications(child_node, new_node)
195+
# Reassign `node` in case the child hook makes a modification
196+
if new_child_and_node.is_a?(Array)
197+
new_node = new_child_and_node[1]
198+
end
199+
end"
200+
end.join("\n")
201+
else
202+
""
203+
end
204+
}
205+
206+
if new_node.equal?(node)
207+
[node, parent]
208+
else
209+
[new_node, parent]
210+
end
211+
end
212+
end
213+
214+
def #{node_method}_with_modifications(node, parent)
215+
new_node_and_new_parent = #{node_method}(node, parent)
216+
apply_modifications(node, parent, new_node_and_new_parent)
217+
end
218+
RUBY
219+
end
220+
219221
[
220222
Language::Nodes::Argument,
221223
Language::Nodes::Directive,
@@ -256,6 +258,8 @@ def on_argument_children(new_node)
256258
make_visit_methods(ast_node_class)
257259
end
258260

261+
# rubocop:enable Development/NoEvalCop
262+
259263
private
260264

261265
def apply_modifications(node, parent, new_node_and_new_parent)

lib/graphql/schema/argument.rb

+3-5
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ def from_resolver?
5353
def initialize(arg_name = nil, type_expr = nil, desc = nil, required: true, type: nil, name: nil, loads: nil, description: nil, ast_node: nil, default_value: NOT_CONFIGURED, as: nil, from_resolver: false, camelize: true, prepare: nil, owner:, validates: nil, directives: nil, deprecation_reason: nil, replace_null_with_default: false, &definition_block)
5454
arg_name ||= name
5555
@name = -(camelize ? Member::BuildType.camelize(arg_name.to_s) : arg_name.to_s)
56+
NameValidator.validate!(@name)
5657
@type_expr = type_expr || type
5758
@description = desc || description
5859
@null = required != true
@@ -88,11 +89,8 @@ def initialize(arg_name = nil, type_expr = nil, desc = nil, required: true, type
8889
end
8990

9091
if definition_block
91-
if definition_block.arity == 1
92-
instance_exec(self, &definition_block)
93-
else
94-
instance_eval(&definition_block)
95-
end
92+
# `self` will still be self, it will also be the first argument to the block:
93+
instance_exec(self, &definition_block)
9694
end
9795
end
9896

lib/graphql/schema/build_from_definition.rb

+8-7
Original file line numberDiff line numberDiff line change
@@ -451,17 +451,18 @@ def build_fields(owner, field_definitions, type_resolver, default_resolve:)
451451

452452
# Don't do this for interfaces
453453
if default_resolve
454-
owner.class_eval <<-RUBY, __FILE__, __LINE__
455-
# frozen_string_literal: true
456-
def #{resolve_method_name}(**args)
457-
field_instance = self.class.get_field("#{field_definition.name}")
458-
context.schema.definition_default_resolve.call(self.class, field_instance, object, args, context)
459-
end
460-
RUBY
454+
define_field_resolve_method(owner, resolve_method_name, field_definition.name)
461455
end
462456
end
463457
end
464458

459+
def define_field_resolve_method(owner, method_name, field_name)
460+
owner.define_method(method_name) { |**args|
461+
field_instance = self.class.get_field(field_name)
462+
context.schema.definition_default_resolve.call(self.class, field_instance, object, args, context)
463+
}
464+
end
465+
465466
def build_resolve_type(lookup_hash, directives, missing_type_handler)
466467
resolve_type_proc = nil
467468
resolve_type_proc = ->(ast_node) {

lib/graphql/schema/directive.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@ def repeatable(new_value)
9999

100100
def inherited(subclass)
101101
super
102-
subclass.class_eval do
102+
subclass.class_exec do
103103
@default_graphql_name ||= nil
104104
end
105105
end

lib/graphql/schema/enum_value.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ def initialize(graphql_name, desc = nil, owner:, ast_node: nil, directives: nil,
4747
end
4848

4949
if block_given?
50-
instance_eval(&block)
50+
instance_exec(self, &block)
5151
end
5252
end
5353

lib/graphql/schema/field.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,7 @@ def initialize(type: nil, name: nil, owner: nil, null: nil, description: NOT_CON
233233

234234
@underscored_name = -Member::BuildType.underscore(name_s)
235235
@name = -(camelize ? Member::BuildType.camelize(name_s) : name_s)
236-
236+
NameValidator.validate!(@name)
237237
@description = description
238238
@type = @owner_type = @own_validators = @own_directives = @own_arguments = @arguments_statically_coercible = nil # these will be prepared later if necessary
239239

0 commit comments

Comments
 (0)