CVE-2025-27407
Description
graphql-ruby is a Ruby implementation of GraphQL. Starting in version 1.11.5 and prior to versions 1.11.8, 1.12.25, 1.13.24, 2.0.32, 2.1.14, 2.2.17, and 2.3.21, loading a malicious schema definition in GraphQL::Schema.from_introspection (or GraphQL::Schema::Loader.load) can result in remote code execution. Any system which loads a schema by JSON from an untrusted source is vulnerable, including those that use GraphQL::Client to load external schemas via GraphQL introspection. Versions 1.11.8, 1.12.25, 1.13.24, 2.0.32, 2.1.14, 2.2.17, and 2.3.21 contain a patch for the issue.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
graphqlRubyGems | >= 2.4.0, < 2.4.13 | 2.4.13 |
graphqlRubyGems | >= 2.3.0, < 2.3.21 | 2.3.21 |
graphqlRubyGems | >= 2.2.0, < 2.2.17 | 2.2.17 |
graphqlRubyGems | >= 2.1.0, < 2.1.15 | 2.1.15 |
graphqlRubyGems | >= 2.0.0, < 2.0.32 | 2.0.32 |
graphqlRubyGems | >= 1.13.0, < 1.13.24 | 1.13.24 |
graphqlRubyGems | >= 1.12.0, < 1.12.25 | 1.12.25 |
graphqlRubyGems | >= 1.11.5, < 1.11.11 | 1.11.11 |
Patches
155f74806f72f8a1e3f51f17adefbbdae81c7af20c5c3c83769069ba310846e5e8e3b04a13887193770b8d5c5a7b9a9bdcMerge commit from fork
30 files changed · +337 −114
benchmark/run.rb+7 −2 modified@@ -110,12 +110,16 @@ def self.build_large_schema end obj_ts = 100.times.map do |n| + input_obj_t = Class.new(GraphQL::Schema::InputObject) do + graphql_name("Input#{n}") + argument :arg, String + end obj_t = Class.new(GraphQL::Schema::Object) do graphql_name("Object#{n}") implements(*int_ts) 20.times do |n2| field :"field#{n2}", String do - argument :arg, String + argument :input, input_obj_t end end @@ -152,8 +156,9 @@ def self.profile_boot end StackProf::Report.new(result).print_text + retained_schema = nil report = MemoryProfiler.report do - build_large_schema + retained_schema = build_large_schema end report.pretty_print
cop/development/no_eval_cop.rb+18 −0 added@@ -0,0 +1,18 @@ +# frozen_string_literal: true +require 'rubocop' + +module Cop + module Development + class NoEvalCop < RuboCop::Cop::Base + 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." + + def on_send(node) + case node.method_name + when :module_eval, :class_eval, :instance_eval + message = MSG_TEMPLATE % { eval_method_name: node.method_name, exec_method_name: node.method_name.to_s.sub("eval", "exec").to_sym } + add_offense node, message: message + end + end + end + end +end
lib/graphql/language/nodes.rb+3 −0 modified@@ -138,6 +138,8 @@ def merge!(new_options) end class << self + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + # Add a default `#visit_method` and `#children_method_name` using the class name def inherited(child_class) super @@ -316,6 +318,7 @@ def self.from_a(filename, line, col, #{(scalar_method_names + @children_methods. RUBY end end + # rubocop:enable Development/NoEvalCop end end
lib/graphql/language/static_visitor.rb+37 −33 modified@@ -22,39 +22,6 @@ def visit end end - # We don't use `alias` here because it breaks `super` - def self.make_visit_methods(ast_node_class) - node_method = ast_node_class.visit_method - children_of_type = ast_node_class.children_of_type - child_visit_method = :"#{node_method}_children" - - class_eval(<<-RUBY, __FILE__, __LINE__ + 1) - # The default implementation for visiting an AST node. - # It doesn't _do_ anything, but it continues to visiting the node's children. - # To customize this hook, override one of its make_visit_methods (or the base method?) - # in your subclasses. - # - # @param node [GraphQL::Language::Nodes::AbstractNode] the node being visited - # @param parent [GraphQL::Language::Nodes::AbstractNode, nil] the previously-visited node, or `nil` if this is the root node. - # @return [void] - def #{node_method}(node, parent) - #{ - if method_defined?(child_visit_method) - "#{child_visit_method}(node)" - elsif children_of_type - children_of_type.map do |child_accessor, child_class| - "node.#{child_accessor}.each do |child_node| - #{child_class.visit_method}(child_node, node) - end" - end.join("\n") - else - "" - end - } - end - RUBY - end - def on_document_children(document_node) document_node.children.each do |child_node| visit_method = child_node.visit_method @@ -123,6 +90,41 @@ def on_argument_children(new_node) end end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + + # We don't use `alias` here because it breaks `super` + def self.make_visit_methods(ast_node_class) + node_method = ast_node_class.visit_method + children_of_type = ast_node_class.children_of_type + child_visit_method = :"#{node_method}_children" + + class_eval(<<-RUBY, __FILE__, __LINE__ + 1) + # The default implementation for visiting an AST node. + # It doesn't _do_ anything, but it continues to visiting the node's children. + # To customize this hook, override one of its make_visit_methods (or the base method?) + # in your subclasses. + # + # @param node [GraphQL::Language::Nodes::AbstractNode] the node being visited + # @param parent [GraphQL::Language::Nodes::AbstractNode, nil] the previously-visited node, or `nil` if this is the root node. + # @return [void] + def #{node_method}(node, parent) + #{ + if method_defined?(child_visit_method) + "#{child_visit_method}(node)" + elsif children_of_type + children_of_type.map do |child_accessor, child_class| + "node.#{child_accessor}.each do |child_node| + #{child_class.visit_method}(child_node, node) + end" + end.join("\n") + else + "" + end + } + end + RUBY + end + [ Language::Nodes::Argument, Language::Nodes::Directive, @@ -162,6 +164,8 @@ def on_argument_children(new_node) ].each do |ast_node_class| make_visit_methods(ast_node_class) end + + # rubocop:disable Development/NoEvalCop end end end
lib/graphql/language/visitor.rb+59 −55 modified@@ -61,61 +61,6 @@ def visit end end - # We don't use `alias` here because it breaks `super` - def self.make_visit_methods(ast_node_class) - node_method = ast_node_class.visit_method - children_of_type = ast_node_class.children_of_type - child_visit_method = :"#{node_method}_children" - - class_eval(<<-RUBY, __FILE__, __LINE__ + 1) - # The default implementation for visiting an AST node. - # It doesn't _do_ anything, but it continues to visiting the node's children. - # To customize this hook, override one of its make_visit_methods (or the base method?) - # in your subclasses. - # - # @param node [GraphQL::Language::Nodes::AbstractNode] the node being visited - # @param parent [GraphQL::Language::Nodes::AbstractNode, nil] the previously-visited node, or `nil` if this is the root node. - # @return [Array, nil] If there were modifications, it returns an array of new nodes, otherwise, it returns `nil`. - def #{node_method}(node, parent) - if node.equal?(DELETE_NODE) - # This might be passed to `super(DELETE_NODE, ...)` - # by a user hook, don't want to keep visiting in that case. - [node, parent] - else - new_node = node - #{ - if method_defined?(child_visit_method) - "new_node = #{child_visit_method}(new_node)" - elsif children_of_type - children_of_type.map do |child_accessor, child_class| - "node.#{child_accessor}.each do |child_node| - new_child_and_node = #{child_class.visit_method}_with_modifications(child_node, new_node) - # Reassign `node` in case the child hook makes a modification - if new_child_and_node.is_a?(Array) - new_node = new_child_and_node[1] - end - end" - end.join("\n") - else - "" - end - } - - if new_node.equal?(node) - [node, parent] - else - [new_node, parent] - end - end - end - - def #{node_method}_with_modifications(node, parent) - new_node_and_new_parent = #{node_method}(node, parent) - apply_modifications(node, parent, new_node_and_new_parent) - end - RUBY - end - def on_document_children(document_node) new_node = document_node document_node.children.each do |child_node| @@ -216,6 +161,63 @@ def on_argument_children(new_node) new_node end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + + # We don't use `alias` here because it breaks `super` + def self.make_visit_methods(ast_node_class) + node_method = ast_node_class.visit_method + children_of_type = ast_node_class.children_of_type + child_visit_method = :"#{node_method}_children" + + class_eval(<<-RUBY, __FILE__, __LINE__ + 1) + # The default implementation for visiting an AST node. + # It doesn't _do_ anything, but it continues to visiting the node's children. + # To customize this hook, override one of its make_visit_methods (or the base method?) + # in your subclasses. + # + # @param node [GraphQL::Language::Nodes::AbstractNode] the node being visited + # @param parent [GraphQL::Language::Nodes::AbstractNode, nil] the previously-visited node, or `nil` if this is the root node. + # @return [Array, nil] If there were modifications, it returns an array of new nodes, otherwise, it returns `nil`. + def #{node_method}(node, parent) + if node.equal?(DELETE_NODE) + # This might be passed to `super(DELETE_NODE, ...)` + # by a user hook, don't want to keep visiting in that case. + [node, parent] + else + new_node = node + #{ + if method_defined?(child_visit_method) + "new_node = #{child_visit_method}(new_node)" + elsif children_of_type + children_of_type.map do |child_accessor, child_class| + "node.#{child_accessor}.each do |child_node| + new_child_and_node = #{child_class.visit_method}_with_modifications(child_node, new_node) + # Reassign `node` in case the child hook makes a modification + if new_child_and_node.is_a?(Array) + new_node = new_child_and_node[1] + end + end" + end.join("\n") + else + "" + end + } + + if new_node.equal?(node) + [node, parent] + else + [new_node, parent] + end + end + end + + def #{node_method}_with_modifications(node, parent) + new_node_and_new_parent = #{node_method}(node, parent) + apply_modifications(node, parent, new_node_and_new_parent) + end + RUBY + end + [ Language::Nodes::Argument, Language::Nodes::Directive, @@ -256,6 +258,8 @@ def on_argument_children(new_node) make_visit_methods(ast_node_class) end + # rubocop:enable Development/NoEvalCop + private def apply_modifications(node, parent, new_node_and_new_parent)
lib/graphql/schema/argument.rb+3 −5 modified@@ -53,6 +53,7 @@ def from_resolver? 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) arg_name ||= name @name = -(camelize ? Member::BuildType.camelize(arg_name.to_s) : arg_name.to_s) + NameValidator.validate!(@name) @type_expr = type_expr || type @description = desc || description @null = required != true @@ -88,11 +89,8 @@ def initialize(arg_name = nil, type_expr = nil, desc = nil, required: true, type end if definition_block - if definition_block.arity == 1 - instance_exec(self, &definition_block) - else - instance_eval(&definition_block) - end + # `self` will still be self, it will also be the first argument to the block: + instance_exec(self, &definition_block) end end
lib/graphql/schema/build_from_definition.rb+8 −7 modified@@ -451,17 +451,18 @@ def build_fields(owner, field_definitions, type_resolver, default_resolve:) # Don't do this for interfaces if default_resolve - owner.class_eval <<-RUBY, __FILE__, __LINE__ - # frozen_string_literal: true - def #{resolve_method_name}(**args) - field_instance = self.class.get_field("#{field_definition.name}") - context.schema.definition_default_resolve.call(self.class, field_instance, object, args, context) - end - RUBY + define_field_resolve_method(owner, resolve_method_name, field_definition.name) end end end + def define_field_resolve_method(owner, method_name, field_name) + owner.define_method(method_name) { |**args| + field_instance = self.class.get_field(field_name) + context.schema.definition_default_resolve.call(self.class, field_instance, object, args, context) + } + end + def build_resolve_type(lookup_hash, directives, missing_type_handler) resolve_type_proc = nil resolve_type_proc = ->(ast_node) {
lib/graphql/schema/directive.rb+1 −1 modified@@ -99,7 +99,7 @@ def repeatable(new_value) def inherited(subclass) super - subclass.class_eval do + subclass.class_exec do @default_graphql_name ||= nil end end
lib/graphql/schema/enum_value.rb+1 −1 modified@@ -47,7 +47,7 @@ def initialize(graphql_name, desc = nil, owner:, ast_node: nil, directives: nil, end if block_given? - instance_eval(&block) + instance_exec(self, &block) end end
lib/graphql/schema/field.rb+1 −1 modified@@ -233,7 +233,7 @@ def initialize(type: nil, name: nil, owner: nil, null: nil, description: NOT_CON @underscored_name = -Member::BuildType.underscore(name_s) @name = -(camelize ? Member::BuildType.camelize(name_s) : name_s) - + NameValidator.validate!(@name) @description = description @type = @owner_type = @own_validators = @own_directives = @own_arguments = @arguments_statically_coercible = nil # these will be prepared later if necessary
lib/graphql/schema/interface.rb+1 −1 modified@@ -29,7 +29,7 @@ def definition_methods(&block) const_set(:DefinitionMethods, defn_methods_module) extend(self::DefinitionMethods) end - self::DefinitionMethods.module_eval(&block) + self::DefinitionMethods.module_exec(&block) end # @see {Schema::Warden} hides interfaces without visible implementations
lib/graphql/schema/member/has_directives.rb+1 −1 modified@@ -6,7 +6,7 @@ class Member module HasDirectives def self.extended(child_cls) super - child_cls.module_eval { self.own_directives = nil } + child_cls.module_exec { self.own_directives = nil } end def inherited(child_cls)
lib/graphql/schema/member/has_fields.rb+1 −1 modified@@ -183,7 +183,7 @@ def self.extended(child_class) def inherited(subclass) super - subclass.class_eval do + subclass.class_exec do @own_fields ||= nil @field_class ||= nil end
lib/graphql/schema/member/has_interfaces.rb+1 −1 modified@@ -133,7 +133,7 @@ def self.extended(child_class) def inherited(subclass) super - subclass.class_eval do + subclass.class_exec do @own_interface_type_memberships ||= nil end end
lib/graphql/schema/member/scoped.rb+1 −1 modified@@ -30,7 +30,7 @@ def reauthorize_scoped_objects(new_value = nil) def inherited(subclass) super - subclass.class_eval do + subclass.class_exec do @reauthorize_scoped_objects = nil end end
lib/graphql/schema/member/type_system_helpers.rb+1 −1 modified@@ -43,7 +43,7 @@ def kind private def inherited(subclass) - subclass.class_eval do + subclass.class_exec do @to_non_null_type ||= nil @to_list_type ||= nil end
lib/graphql/tracing/appoptics_trace.rb+4 −0 modified@@ -28,6 +28,8 @@ def self.version Gem::Version.new('1.0.0') end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + [ 'lex', 'parse', @@ -55,6 +57,8 @@ def #{trace_method}(**data) RUBY end + # rubocop:enable Development/NoEvalCop + def execute_field(query:, field:, ast_node:, arguments:, object:) return_type = field.type.unwrap trace_field = if return_type.kind.scalar? || return_type.kind.enum?
lib/graphql/tracing/appsignal_trace.rb+4 −0 modified@@ -13,6 +13,8 @@ def initialize(set_action_name: false, **rest) super end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { "lex" => "lex.graphql", "parse" => "parse.graphql", @@ -43,6 +45,8 @@ def #{trace_method}(**data) RUBY end + # rubocop:enable Development/NoEvalCop + def platform_execute_field(platform_key) Appsignal.instrument(platform_key) do yield
lib/graphql/tracing/data_dog_trace.rb+4 −0 modified@@ -20,6 +20,8 @@ def initialize(tracer: nil, analytics_enabled: false, analytics_sample_rate: 1.0 super end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { 'lex' => 'lex.graphql', 'parse' => 'parse.graphql', @@ -69,6 +71,8 @@ def #{trace_method}(**data) RUBY end + # rubocop:enable Development/NoEvalCop + def execute_field_span(span_key, query, field, ast_node, arguments, object) return_type = field.type.unwrap trace_field = if return_type.kind.scalar? || return_type.kind.enum?
lib/graphql/tracing/notifications_trace.rb+4 −0 modified@@ -16,6 +16,8 @@ def initialize(engine:, **rest) super end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { "lex" => "lex.graphql", "parse" => "parse.graphql", @@ -39,6 +41,8 @@ def #{trace_method}(**metadata, &blk) RUBY end + # rubocop:enable Development/NoEvalCop + include PlatformTrace end end
lib/graphql/tracing/platform_trace.rb+5 −0 modified@@ -39,6 +39,9 @@ def self.included(child_class) include(BaseKeyCache) } child_class.const_set(:KeyCache, key_methods_class) + + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + [:execute_field, :execute_field_lazy].each do |field_trace_method| if !child_class.method_defined?(field_trace_method) child_class.module_eval <<-RUBY, __FILE__, __LINE__ @@ -91,6 +94,8 @@ def #{rt_trace_method}(query:, type:, object:) end RUBY end + + # rubocop:enable Development/NoEvalCop end end
lib/graphql/tracing/prometheus_trace.rb+4 −0 modified@@ -13,6 +13,8 @@ def initialize(client: PrometheusExporter::Client.default, keys_whitelist: ["exe super(**rest) end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { 'lex' => "graphql.lex", 'parse' => "graphql.parse", @@ -30,6 +32,8 @@ def #{trace_method}(**data, &block) RUBY end + # rubocop:enable Development/NoEvalCop + def platform_execute_field(platform_key, &block) instrument_execution(platform_key, "execute_field", &block) end
lib/graphql/tracing/scout_trace.rb+3 −0 modified@@ -16,6 +16,8 @@ def initialize(set_transaction_name: false, **_rest) super end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { "lex" => "lex.graphql", "parse" => "parse.graphql", @@ -45,6 +47,7 @@ def #{trace_method}(**data) end RUBY end + # rubocop:enable Development/NoEvalCop def platform_execute_field(platform_key, &block) self.class.instrument("GraphQL", platform_key, INSTRUMENT_OPTS, &block)
lib/graphql/tracing/sentry_trace.rb+4 −0 modified@@ -23,6 +23,8 @@ def execute_query(**data) instrument_execution("graphql.execute", "execute_query", data) { super } end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { "lex" => "graphql.lex", "parse" => "graphql.parse", @@ -39,6 +41,8 @@ def #{trace_method}(**data) RUBY end + # rubocop:enable Development/NoEvalCop + def platform_execute_field(platform_key, &block) instrument_execution(platform_key, "execute_field", &block) end
lib/graphql/tracing/statsd_trace.rb+4 −0 modified@@ -11,6 +11,8 @@ def initialize(statsd:, **rest) super(**rest) end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { 'lex' => "graphql.lex", 'parse' => "graphql.parse", @@ -30,6 +32,8 @@ def #{trace_method}(**data) RUBY end + # rubocop:enable Development/NoEvalCop + def platform_execute_field(platform_key, &block) @statsd.time(platform_key, &block) end
lib/graphql/types/relay/connection_behaviors.rb+1 −1 modified@@ -13,7 +13,7 @@ def self.included(child_class) child_class.node_nullable(true) child_class.edges_nullable(true) child_class.edge_nullable(true) - child_class.module_eval { + child_class.module_exec { self.edge_type = nil self.node_type = nil self.edge_class = nil
lib/graphql/types/relay/edge_behaviors.rb+1 −1 modified@@ -8,7 +8,7 @@ def self.included(child_class) child_class.description("An edge in a connection.") child_class.field(:cursor, String, null: false, description: "A cursor for use in pagination.") child_class.extend(ClassMethods) - child_class.class_eval { self.node_type = nil } + child_class.class_exec { self.node_type = nil } child_class.node_nullable(true) end
lib/graphql/version.rb+1 −1 modified@@ -1,4 +1,4 @@ # frozen_string_literal: true module GraphQL - VERSION = "2.2.16" + VERSION = "2.2.17" end
.rubocop.yml+5 −0 modified@@ -1,5 +1,6 @@ require: - ./cop/development/none_without_block_cop + - ./cop/development/no_eval_cop - ./cop/development/no_focus_cop - ./lib/graphql/rubocop/graphql/default_null_true - ./lib/graphql/rubocop/graphql/default_required_true @@ -51,6 +52,10 @@ Development/NoneWithoutBlockCop: - "lib/**/*" - "spec/**/*" +Development/NoEvalCop: + Include: + - "lib/**/*" + Development/NoFocusCop: Include: - "spec/**/*"
spec/graphql/schema/loader_spec.rb+149 −0 modified@@ -398,4 +398,153 @@ def assert_deep_equal(expected_type, actual_type) assert_equal arg.default_value, { 'id' => nil, 'int' => nil, 'float' => nil, 'enum' => nil, 'sub' => nil, 'bool' => nil, 'bigint' => nil } end end + + it "validates field argument names" do + json = { + "data" => { + "__schema" => { + "queryType" => { + "name" => "Query" + }, + "mutationType" => nil, + "subscriptionType" => nil, + "types" => [ + { + "kind" => "OBJECT", + "name" => "Query", + "description" => nil, + "fields" => [ + { + "name" => "int", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil, + }, + "args" => [ + { + "name" => "something-wrong", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil + }, + "defaultValue" => nil + } + ], + } + ] + } + ] + } + } + } + err = assert_raises GraphQL::InvalidNameError do + GraphQL::Schema.from_introspection(json) + end + + assert_includes err.message, "something-wrong" + end + + it "validates field names" do + json = { + "data" => { + "__schema" => { + "queryType" => { + "name" => "Query" + }, + "mutationType" => nil, + "subscriptionType" => nil, + "types" => [ + { + "kind" => "OBJECT", + "name" => "Query", + "description" => nil, + "fields" => [ + { + "name" => "bad.int", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil, + }, + "args" => [], + } + ] + } + ] + } + } + } + err = assert_raises GraphQL::InvalidNameError do + GraphQL::Schema.from_introspection(json) + end + + assert_includes err.message, "bad.int" + end + + it "validates input object argument names" do + json = { + "data" => { + "__schema" => { + "queryType" => { + "name" => "Query" + }, + "mutationType" => nil, + "subscriptionType" => nil, + "types" => [ + { + "kind" => "OBJECT", + "name" => "Query", + "description" => nil, + "fields" => [ + { + "name" => "int", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil, + }, + "args" => [ + { + "name" => "inputObject", + "description" => nil, + "type" => { + "kind" => "INPUT_OBJECT", + "name" => "SomeInputObject", + "ofType" => nil + }, + "defaultValue" => nil + } + ], + } + ] + }, + { + "kind" => "INPUT_OBJECT", + "name" => "SomeInputObject", + "description" => nil, + "inputFields" => [ + { + "name"=>"bad, input", + "type"=> { "kind" => "SCALAR", "name" => "String"}, + "defaultValue"=> nil, + "description" => nil, + }, + ] + } + ] + } + } + } + err = assert_raises GraphQL::InvalidNameError do + GraphQL::Schema.from_introspection(json) + end + + assert_includes err.message, "bad, input" + end end
d1117ae0361dMerge commit from fork
30 files changed · +354 −120
benchmark/run.rb+7 −2 modified@@ -110,12 +110,16 @@ def self.build_large_schema end obj_ts = 100.times.map do |n| + input_obj_t = Class.new(GraphQL::Schema::InputObject) do + graphql_name("Input#{n}") + argument :arg, String + end obj_t = Class.new(GraphQL::Schema::Object) do graphql_name("Object#{n}") implements(*int_ts) 20.times do |n2| field :"field#{n2}", String do - argument :arg, String + argument :input, input_obj_t end end @@ -152,8 +156,9 @@ def self.profile_boot end StackProf::Report.new(result).print_text + retained_schema = nil report = MemoryProfiler.report do - build_large_schema + retained_schema = build_large_schema end report.pretty_print
cop/development/no_eval_cop.rb+18 −0 added@@ -0,0 +1,18 @@ +# frozen_string_literal: true +require 'rubocop' + +module Cop + module Development + class NoEvalCop < RuboCop::Cop::Base + 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." + + def on_send(node) + case node.method_name + when :module_eval, :class_eval, :instance_eval + message = MSG_TEMPLATE % { eval_method_name: node.method_name, exec_method_name: node.method_name.to_s.sub("eval", "exec").to_sym } + add_offense node, message: message + end + end + end + end +end
lib/graphql/language/nodes.rb+3 −0 modified@@ -138,6 +138,8 @@ def merge!(new_options) end class << self + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + # Add a default `#visit_method` and `#children_method_name` using the class name def inherited(child_class) super @@ -296,6 +298,7 @@ def self.from_a(filename, line, col, #{(scalar_method_names + @children_methods. RUBY end end + # rubocop:enable Development/NoEvalCop end end
lib/graphql/language/static_visitor.rb+37 −33 modified@@ -22,39 +22,6 @@ def visit end end - # We don't use `alias` here because it breaks `super` - def self.make_visit_methods(ast_node_class) - node_method = ast_node_class.visit_method - children_of_type = ast_node_class.children_of_type - child_visit_method = :"#{node_method}_children" - - class_eval(<<-RUBY, __FILE__, __LINE__ + 1) - # The default implementation for visiting an AST node. - # It doesn't _do_ anything, but it continues to visiting the node's children. - # To customize this hook, override one of its make_visit_methods (or the base method?) - # in your subclasses. - # - # @param node [GraphQL::Language::Nodes::AbstractNode] the node being visited - # @param parent [GraphQL::Language::Nodes::AbstractNode, nil] the previously-visited node, or `nil` if this is the root node. - # @return [void] - def #{node_method}(node, parent) - #{ - if method_defined?(child_visit_method) - "#{child_visit_method}(node)" - elsif children_of_type - children_of_type.map do |child_accessor, child_class| - "node.#{child_accessor}.each do |child_node| - #{child_class.visit_method}(child_node, node) - end" - end.join("\n") - else - "" - end - } - end - RUBY - end - def on_document_children(document_node) document_node.children.each do |child_node| visit_method = child_node.visit_method @@ -123,6 +90,41 @@ def on_argument_children(new_node) end end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + + # We don't use `alias` here because it breaks `super` + def self.make_visit_methods(ast_node_class) + node_method = ast_node_class.visit_method + children_of_type = ast_node_class.children_of_type + child_visit_method = :"#{node_method}_children" + + class_eval(<<-RUBY, __FILE__, __LINE__ + 1) + # The default implementation for visiting an AST node. + # It doesn't _do_ anything, but it continues to visiting the node's children. + # To customize this hook, override one of its make_visit_methods (or the base method?) + # in your subclasses. + # + # @param node [GraphQL::Language::Nodes::AbstractNode] the node being visited + # @param parent [GraphQL::Language::Nodes::AbstractNode, nil] the previously-visited node, or `nil` if this is the root node. + # @return [void] + def #{node_method}(node, parent) + #{ + if method_defined?(child_visit_method) + "#{child_visit_method}(node)" + elsif children_of_type + children_of_type.map do |child_accessor, child_class| + "node.#{child_accessor}.each do |child_node| + #{child_class.visit_method}(child_node, node) + end" + end.join("\n") + else + "" + end + } + end + RUBY + end + [ Language::Nodes::Argument, Language::Nodes::Directive, @@ -162,6 +164,8 @@ def on_argument_children(new_node) ].each do |ast_node_class| make_visit_methods(ast_node_class) end + + # rubocop:disable Development/NoEvalCop end end end
lib/graphql/language/visitor.rb+59 −55 modified@@ -61,61 +61,6 @@ def visit end end - # We don't use `alias` here because it breaks `super` - def self.make_visit_methods(ast_node_class) - node_method = ast_node_class.visit_method - children_of_type = ast_node_class.children_of_type - child_visit_method = :"#{node_method}_children" - - class_eval(<<-RUBY, __FILE__, __LINE__ + 1) - # The default implementation for visiting an AST node. - # It doesn't _do_ anything, but it continues to visiting the node's children. - # To customize this hook, override one of its make_visit_methods (or the base method?) - # in your subclasses. - # - # @param node [GraphQL::Language::Nodes::AbstractNode] the node being visited - # @param parent [GraphQL::Language::Nodes::AbstractNode, nil] the previously-visited node, or `nil` if this is the root node. - # @return [Array, nil] If there were modifications, it returns an array of new nodes, otherwise, it returns `nil`. - def #{node_method}(node, parent) - if node.equal?(DELETE_NODE) - # This might be passed to `super(DELETE_NODE, ...)` - # by a user hook, don't want to keep visiting in that case. - [node, parent] - else - new_node = node - #{ - if method_defined?(child_visit_method) - "new_node = #{child_visit_method}(new_node)" - elsif children_of_type - children_of_type.map do |child_accessor, child_class| - "node.#{child_accessor}.each do |child_node| - new_child_and_node = #{child_class.visit_method}_with_modifications(child_node, new_node) - # Reassign `node` in case the child hook makes a modification - if new_child_and_node.is_a?(Array) - new_node = new_child_and_node[1] - end - end" - end.join("\n") - else - "" - end - } - - if new_node.equal?(node) - [node, parent] - else - [new_node, parent] - end - end - end - - def #{node_method}_with_modifications(node, parent) - new_node_and_new_parent = #{node_method}(node, parent) - apply_modifications(node, parent, new_node_and_new_parent) - end - RUBY - end - def on_document_children(document_node) new_node = document_node document_node.children.each do |child_node| @@ -216,6 +161,63 @@ def on_argument_children(new_node) new_node end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + + # We don't use `alias` here because it breaks `super` + def self.make_visit_methods(ast_node_class) + node_method = ast_node_class.visit_method + children_of_type = ast_node_class.children_of_type + child_visit_method = :"#{node_method}_children" + + class_eval(<<-RUBY, __FILE__, __LINE__ + 1) + # The default implementation for visiting an AST node. + # It doesn't _do_ anything, but it continues to visiting the node's children. + # To customize this hook, override one of its make_visit_methods (or the base method?) + # in your subclasses. + # + # @param node [GraphQL::Language::Nodes::AbstractNode] the node being visited + # @param parent [GraphQL::Language::Nodes::AbstractNode, nil] the previously-visited node, or `nil` if this is the root node. + # @return [Array, nil] If there were modifications, it returns an array of new nodes, otherwise, it returns `nil`. + def #{node_method}(node, parent) + if node.equal?(DELETE_NODE) + # This might be passed to `super(DELETE_NODE, ...)` + # by a user hook, don't want to keep visiting in that case. + [node, parent] + else + new_node = node + #{ + if method_defined?(child_visit_method) + "new_node = #{child_visit_method}(new_node)" + elsif children_of_type + children_of_type.map do |child_accessor, child_class| + "node.#{child_accessor}.each do |child_node| + new_child_and_node = #{child_class.visit_method}_with_modifications(child_node, new_node) + # Reassign `node` in case the child hook makes a modification + if new_child_and_node.is_a?(Array) + new_node = new_child_and_node[1] + end + end" + end.join("\n") + else + "" + end + } + + if new_node.equal?(node) + [node, parent] + else + [new_node, parent] + end + end + end + + def #{node_method}_with_modifications(node, parent) + new_node_and_new_parent = #{node_method}(node, parent) + apply_modifications(node, parent, new_node_and_new_parent) + end + RUBY + end + [ Language::Nodes::Argument, Language::Nodes::Directive, @@ -256,6 +258,8 @@ def on_argument_children(new_node) make_visit_methods(ast_node_class) end + # rubocop:enable Development/NoEvalCop + private def apply_modifications(node, parent, new_node_and_new_parent)
lib/graphql/schema/argument.rb+3 −5 modified@@ -53,6 +53,7 @@ def from_resolver? 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) arg_name ||= name @name = -(camelize ? Member::BuildType.camelize(arg_name.to_s) : arg_name.to_s) + NameValidator.validate!(@name) @type_expr = type_expr || type @description = desc || description @null = required != true @@ -88,11 +89,8 @@ def initialize(arg_name = nil, type_expr = nil, desc = nil, required: true, type end if definition_block - if definition_block.arity == 1 - instance_exec(self, &definition_block) - else - instance_eval(&definition_block) - end + # `self` will still be self, it will also be the first argument to the block: + instance_exec(self, &definition_block) end end
lib/graphql/schema/build_from_definition.rb+8 −7 modified@@ -451,17 +451,18 @@ def build_fields(owner, field_definitions, type_resolver, default_resolve:) # Don't do this for interfaces if default_resolve - owner.class_eval <<-RUBY, __FILE__, __LINE__ - # frozen_string_literal: true - def #{resolve_method_name}(**args) - field_instance = self.class.get_field("#{field_definition.name}") - context.schema.definition_default_resolve.call(self.class, field_instance, object, args, context) - end - RUBY + define_field_resolve_method(owner, resolve_method_name, field_definition.name) end end end + def define_field_resolve_method(owner, method_name, field_name) + owner.define_method(method_name) { |**args| + field_instance = self.class.get_field(field_name) + context.schema.definition_default_resolve.call(self.class, field_instance, object, args, context) + } + end + def build_resolve_type(lookup_hash, directives, missing_type_handler) resolve_type_proc = nil resolve_type_proc = ->(ast_node) {
lib/graphql/schema/directive.rb+1 −1 modified@@ -99,7 +99,7 @@ def repeatable(new_value) def inherited(subclass) super - subclass.class_eval do + subclass.class_exec do @default_graphql_name ||= nil end end
lib/graphql/schema/enum_value.rb+1 −1 modified@@ -47,7 +47,7 @@ def initialize(graphql_name, desc = nil, owner:, ast_node: nil, directives: nil, end if block_given? - instance_eval(&block) + instance_exec(self, &block) end end
lib/graphql/schema/field.rb+1 −1 modified@@ -233,7 +233,7 @@ def initialize(type: nil, name: nil, owner: nil, null: nil, description: NOT_CON @underscored_name = -Member::BuildType.underscore(name_s) @name = -(camelize ? Member::BuildType.camelize(name_s) : name_s) - + NameValidator.validate!(@name) @description = description @type = @owner_type = @own_validators = @own_directives = @own_arguments = @arguments_statically_coercible = nil # these will be prepared later if necessary
lib/graphql/schema/input_object.rb+21 −6 modified@@ -44,6 +44,19 @@ def to_hash to_h end +<<<<<<< HEAD +======= + def deconstruct_keys(keys = nil) + if keys.nil? + @ruby_style_hash + else + new_h = {} + keys.each { |k| @ruby_style_hash.key?(k) && new_h[k] = @ruby_style_hash[k] } + new_h + end + end + +>>>>>>> d85e69690f (Disable development NoEvalCop for load-time eval calls) def prepare if @context object = @context[:current_object] @@ -131,12 +144,7 @@ def argument(*args, **kwargs, &block) end end # Add a method access - method_name = argument_defn.keyword - class_eval <<-RUBY, __FILE__, __LINE__ - def #{method_name} - self[#{method_name.inspect}] - end - RUBY + define_accessor_method(argument_defn.keyword) argument_defn end @@ -242,6 +250,13 @@ def coerce_result(value, ctx) result end + + private + + def define_accessor_method(method_name) + define_method(method_name) { self[method_name] } + alias_method(method_name, method_name) + end end private
lib/graphql/schema/interface.rb+1 −1 modified@@ -29,7 +29,7 @@ def definition_methods(&block) const_set(:DefinitionMethods, defn_methods_module) extend(self::DefinitionMethods) end - self::DefinitionMethods.module_eval(&block) + self::DefinitionMethods.module_exec(&block) end # @see {Schema::Warden} hides interfaces without visible implementations
lib/graphql/schema/member/has_directives.rb+1 −1 modified@@ -6,7 +6,7 @@ class Member module HasDirectives def self.extended(child_cls) super - child_cls.module_eval { self.own_directives = nil } + child_cls.module_exec { self.own_directives = nil } end def inherited(child_cls)
lib/graphql/schema/member/has_fields.rb+1 −1 modified@@ -183,7 +183,7 @@ def self.extended(child_class) def inherited(subclass) super - subclass.class_eval do + subclass.class_exec do @own_fields ||= nil @field_class ||= nil end
lib/graphql/schema/member/has_interfaces.rb+1 −1 modified@@ -133,7 +133,7 @@ def self.extended(child_class) def inherited(subclass) super - subclass.class_eval do + subclass.class_exec do @own_interface_type_memberships ||= nil end end
lib/graphql/schema/member/scoped.rb+1 −1 modified@@ -30,7 +30,7 @@ def reauthorize_scoped_objects(new_value = nil) def inherited(subclass) super - subclass.class_eval do + subclass.class_exec do @reauthorize_scoped_objects = nil end end
lib/graphql/schema/member/type_system_helpers.rb+1 −1 modified@@ -43,7 +43,7 @@ def kind private def inherited(subclass) - subclass.class_eval do + subclass.class_exec do @to_non_null_type ||= nil @to_list_type ||= nil end
lib/graphql/tracing/appoptics_trace.rb+4 −0 modified@@ -28,6 +28,8 @@ def self.version Gem::Version.new('1.0.0') end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + [ 'lex', 'parse', @@ -55,6 +57,8 @@ def #{trace_method}(**data) RUBY end + # rubocop:enable Development/NoEvalCop + def execute_field(query:, field:, ast_node:, arguments:, object:) return_type = field.type.unwrap trace_field = if return_type.kind.scalar? || return_type.kind.enum?
lib/graphql/tracing/appsignal_trace.rb+4 −0 modified@@ -13,6 +13,8 @@ def initialize(set_action_name: false, **rest) super end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { "lex" => "lex.graphql", "parse" => "parse.graphql", @@ -43,6 +45,8 @@ def #{trace_method}(**data) RUBY end + # rubocop:enable Development/NoEvalCop + def platform_execute_field(platform_key) Appsignal.instrument(platform_key) do yield
lib/graphql/tracing/data_dog_trace.rb+4 −0 modified@@ -20,6 +20,8 @@ def initialize(tracer: nil, analytics_enabled: false, analytics_sample_rate: 1.0 super end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { 'lex' => 'lex.graphql', 'parse' => 'parse.graphql', @@ -69,6 +71,8 @@ def #{trace_method}(**data) RUBY end + # rubocop:enable Development/NoEvalCop + def execute_field_span(span_key, query, field, ast_node, arguments, object) return_type = field.type.unwrap trace_field = if return_type.kind.scalar? || return_type.kind.enum?
lib/graphql/tracing/notifications_trace.rb+4 −0 modified@@ -16,6 +16,8 @@ def initialize(engine:, **rest) super end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { "lex" => "lex.graphql", "parse" => "parse.graphql", @@ -39,6 +41,8 @@ def #{trace_method}(**metadata, &blk) RUBY end + # rubocop:enable Development/NoEvalCop + include PlatformTrace end end
lib/graphql/tracing/platform_trace.rb+5 −0 modified@@ -39,6 +39,9 @@ def self.included(child_class) include(BaseKeyCache) } child_class.const_set(:KeyCache, key_methods_class) + + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + [:execute_field, :execute_field_lazy].each do |field_trace_method| if !child_class.method_defined?(field_trace_method) child_class.module_eval <<-RUBY, __FILE__, __LINE__ @@ -91,6 +94,8 @@ def #{rt_trace_method}(query:, type:, object:) end RUBY end + + # rubocop:enable Development/NoEvalCop end end
lib/graphql/tracing/prometheus_trace.rb+4 −0 modified@@ -13,6 +13,8 @@ def initialize(client: PrometheusExporter::Client.default, keys_whitelist: ["exe super(**rest) end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { 'lex' => "graphql.lex", 'parse' => "graphql.parse", @@ -30,6 +32,8 @@ def #{trace_method}(**data, &block) RUBY end + # rubocop:enable Development/NoEvalCop + def platform_execute_field(platform_key, &block) instrument_execution(platform_key, "execute_field", &block) end
lib/graphql/tracing/scout_trace.rb+3 −0 modified@@ -16,6 +16,8 @@ def initialize(set_transaction_name: false, **_rest) super end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { "lex" => "lex.graphql", "parse" => "parse.graphql", @@ -45,6 +47,7 @@ def #{trace_method}(**data) end RUBY end + # rubocop:enable Development/NoEvalCop def platform_execute_field(platform_key, &block) self.class.instrument("GraphQL", platform_key, INSTRUMENT_OPTS, &block)
lib/graphql/tracing/statsd_trace.rb+4 −0 modified@@ -11,6 +11,8 @@ def initialize(statsd:, **rest) super(**rest) end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { 'lex' => "graphql.lex", 'parse' => "graphql.parse", @@ -30,6 +32,8 @@ def #{trace_method}(**data) RUBY end + # rubocop:enable Development/NoEvalCop + def platform_execute_field(platform_key, &block) @statsd.time(platform_key, &block) end
lib/graphql/types/relay/connection_behaviors.rb+1 −1 modified@@ -13,7 +13,7 @@ def self.included(child_class) child_class.node_nullable(true) child_class.edges_nullable(true) child_class.edge_nullable(true) - child_class.module_eval { + child_class.module_exec { self.edge_type = nil self.node_type = nil self.edge_class = nil
lib/graphql/types/relay/edge_behaviors.rb+1 −1 modified@@ -8,7 +8,7 @@ def self.included(child_class) child_class.description("An edge in a connection.") child_class.field(:cursor, String, null: false, description: "A cursor for use in pagination.") child_class.extend(ClassMethods) - child_class.class_eval { self.node_type = nil } + child_class.class_exec { self.node_type = nil } child_class.node_nullable(true) end
lib/graphql/version.rb+1 −1 modified@@ -1,4 +1,4 @@ # frozen_string_literal: true module GraphQL - VERSION = "2.1.13" + VERSION = "2.1.14" end
.rubocop.yml+5 −0 modified@@ -1,5 +1,6 @@ require: - ./cop/development/none_without_block_cop + - ./cop/development/no_eval_cop - ./cop/development/no_focus_cop - ./lib/graphql/rubocop/graphql/default_null_true - ./lib/graphql/rubocop/graphql/default_required_true @@ -51,6 +52,10 @@ Development/NoneWithoutBlockCop: - "lib/**/*" - "spec/**/*" +Development/NoEvalCop: + Include: + - "lib/**/*" + Development/NoFocusCop: Include: - "spec/**/*"
spec/graphql/schema/loader_spec.rb+149 −0 modified@@ -398,4 +398,153 @@ def assert_deep_equal(expected_type, actual_type) assert_equal arg.default_value, { 'id' => nil, 'int' => nil, 'float' => nil, 'enum' => nil, 'sub' => nil, 'bool' => nil, 'bigint' => nil } end end + + it "validates field argument names" do + json = { + "data" => { + "__schema" => { + "queryType" => { + "name" => "Query" + }, + "mutationType" => nil, + "subscriptionType" => nil, + "types" => [ + { + "kind" => "OBJECT", + "name" => "Query", + "description" => nil, + "fields" => [ + { + "name" => "int", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil, + }, + "args" => [ + { + "name" => "something-wrong", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil + }, + "defaultValue" => nil + } + ], + } + ] + } + ] + } + } + } + err = assert_raises GraphQL::InvalidNameError do + GraphQL::Schema.from_introspection(json) + end + + assert_includes err.message, "something-wrong" + end + + it "validates field names" do + json = { + "data" => { + "__schema" => { + "queryType" => { + "name" => "Query" + }, + "mutationType" => nil, + "subscriptionType" => nil, + "types" => [ + { + "kind" => "OBJECT", + "name" => "Query", + "description" => nil, + "fields" => [ + { + "name" => "bad.int", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil, + }, + "args" => [], + } + ] + } + ] + } + } + } + err = assert_raises GraphQL::InvalidNameError do + GraphQL::Schema.from_introspection(json) + end + + assert_includes err.message, "bad.int" + end + + it "validates input object argument names" do + json = { + "data" => { + "__schema" => { + "queryType" => { + "name" => "Query" + }, + "mutationType" => nil, + "subscriptionType" => nil, + "types" => [ + { + "kind" => "OBJECT", + "name" => "Query", + "description" => nil, + "fields" => [ + { + "name" => "int", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil, + }, + "args" => [ + { + "name" => "inputObject", + "description" => nil, + "type" => { + "kind" => "INPUT_OBJECT", + "name" => "SomeInputObject", + "ofType" => nil + }, + "defaultValue" => nil + } + ], + } + ] + }, + { + "kind" => "INPUT_OBJECT", + "name" => "SomeInputObject", + "description" => nil, + "inputFields" => [ + { + "name"=>"bad, input", + "type"=> { "kind" => "SCALAR", "name" => "String"}, + "defaultValue"=> nil, + "description" => nil, + }, + ] + } + ] + } + } + } + err = assert_raises GraphQL::InvalidNameError do + GraphQL::Schema.from_introspection(json) + end + + assert_includes err.message, "bad, input" + end end
e3b33ace0539Merge commit from fork
33 files changed · +348 −123
benchmark/run.rb+7 −2 modified@@ -110,12 +110,16 @@ def self.build_large_schema end obj_ts = 100.times.map do |n| + input_obj_t = Class.new(GraphQL::Schema::InputObject) do + graphql_name("Input#{n}") + argument :arg, String + end obj_t = Class.new(GraphQL::Schema::Object) do graphql_name("Object#{n}") implements(*int_ts) 20.times do |n2| field :"field#{n2}", String do - argument :arg, String + argument :input, input_obj_t end end @@ -152,8 +156,9 @@ def self.profile_boot end StackProf::Report.new(result).print_text + retained_schema = nil report = MemoryProfiler.report do - build_large_schema + retained_schema = build_large_schema end report.pretty_print
cop/development/no_eval_cop.rb+18 −0 added@@ -0,0 +1,18 @@ +# frozen_string_literal: true +require 'rubocop' + +module Cop + module Development + class NoEvalCop < RuboCop::Cop::Base + 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." + + def on_send(node) + case node.method_name + when :module_eval, :class_eval, :instance_eval + message = MSG_TEMPLATE % { eval_method_name: node.method_name, exec_method_name: node.method_name.to_s.sub("eval", "exec").to_sym } + add_offense node, message: message + end + end + end + end +end
lib/graphql/analysis/analyzer.rb+2 −1 modified@@ -42,6 +42,7 @@ def result raise GraphQL::RequiredImplementationMissingError end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time class << self private @@ -72,7 +73,7 @@ def on_leave_#{member_name}(node, parent, visitor) build_visitor_hooks :variable_definition build_visitor_hooks :variable_identifier build_visitor_hooks :abstract_node - + # rubocop:enable Development/NoEvalCop protected # @return [GraphQL::Query, GraphQL::Execution::Multiplex] Whatever this analyzer is analyzing
lib/graphql/analysis/visitor.rb+2 −0 modified@@ -64,6 +64,7 @@ def response_path @response_path.dup end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time # Visitor Hooks [ :operation_definition, :fragment_definition, @@ -92,6 +93,7 @@ def call_on_leave_#{node_type}(node, parent) RUBY end + # rubocop:enable Development/NoEvalCop def on_operation_definition(node, parent) object_type = @schema.root_type_for_operation(node.operation_type)
lib/graphql/language/nodes.rb+3 −0 modified@@ -141,6 +141,8 @@ def merge!(new_options) end class << self + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + # Add a default `#visit_method` and `#children_method_name` using the class name def inherited(child_class) super @@ -343,6 +345,7 @@ def marshal_load(values) RUBY end end + # rubocop:enable Development/NoEvalCop end end
lib/graphql/language/static_visitor.rb+37 −33 modified@@ -22,39 +22,6 @@ def visit end end - # We don't use `alias` here because it breaks `super` - def self.make_visit_methods(ast_node_class) - node_method = ast_node_class.visit_method - children_of_type = ast_node_class.children_of_type - child_visit_method = :"#{node_method}_children" - - class_eval(<<-RUBY, __FILE__, __LINE__ + 1) - # The default implementation for visiting an AST node. - # It doesn't _do_ anything, but it continues to visiting the node's children. - # To customize this hook, override one of its make_visit_methods (or the base method?) - # in your subclasses. - # - # @param node [GraphQL::Language::Nodes::AbstractNode] the node being visited - # @param parent [GraphQL::Language::Nodes::AbstractNode, nil] the previously-visited node, or `nil` if this is the root node. - # @return [void] - def #{node_method}(node, parent) - #{ - if method_defined?(child_visit_method) - "#{child_visit_method}(node)" - elsif children_of_type - children_of_type.map do |child_accessor, child_class| - "node.#{child_accessor}.each do |child_node| - #{child_class.visit_method}(child_node, node) - end" - end.join("\n") - else - "" - end - } - end - RUBY - end - def on_document_children(document_node) document_node.children.each do |child_node| visit_method = child_node.visit_method @@ -123,6 +90,41 @@ def on_argument_children(new_node) end end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + + # We don't use `alias` here because it breaks `super` + def self.make_visit_methods(ast_node_class) + node_method = ast_node_class.visit_method + children_of_type = ast_node_class.children_of_type + child_visit_method = :"#{node_method}_children" + + class_eval(<<-RUBY, __FILE__, __LINE__ + 1) + # The default implementation for visiting an AST node. + # It doesn't _do_ anything, but it continues to visiting the node's children. + # To customize this hook, override one of its make_visit_methods (or the base method?) + # in your subclasses. + # + # @param node [GraphQL::Language::Nodes::AbstractNode] the node being visited + # @param parent [GraphQL::Language::Nodes::AbstractNode, nil] the previously-visited node, or `nil` if this is the root node. + # @return [void] + def #{node_method}(node, parent) + #{ + if method_defined?(child_visit_method) + "#{child_visit_method}(node)" + elsif children_of_type + children_of_type.map do |child_accessor, child_class| + "node.#{child_accessor}.each do |child_node| + #{child_class.visit_method}(child_node, node) + end" + end.join("\n") + else + "" + end + } + end + RUBY + end + [ Language::Nodes::Argument, Language::Nodes::Directive, @@ -162,6 +164,8 @@ def on_argument_children(new_node) ].each do |ast_node_class| make_visit_methods(ast_node_class) end + + # rubocop:disable Development/NoEvalCop end end end
lib/graphql/language/visitor.rb+59 −55 modified@@ -61,61 +61,6 @@ def visit end end - # We don't use `alias` here because it breaks `super` - def self.make_visit_methods(ast_node_class) - node_method = ast_node_class.visit_method - children_of_type = ast_node_class.children_of_type - child_visit_method = :"#{node_method}_children" - - class_eval(<<-RUBY, __FILE__, __LINE__ + 1) - # The default implementation for visiting an AST node. - # It doesn't _do_ anything, but it continues to visiting the node's children. - # To customize this hook, override one of its make_visit_methods (or the base method?) - # in your subclasses. - # - # @param node [GraphQL::Language::Nodes::AbstractNode] the node being visited - # @param parent [GraphQL::Language::Nodes::AbstractNode, nil] the previously-visited node, or `nil` if this is the root node. - # @return [Array, nil] If there were modifications, it returns an array of new nodes, otherwise, it returns `nil`. - def #{node_method}(node, parent) - if node.equal?(DELETE_NODE) - # This might be passed to `super(DELETE_NODE, ...)` - # by a user hook, don't want to keep visiting in that case. - [node, parent] - else - new_node = node - #{ - if method_defined?(child_visit_method) - "new_node = #{child_visit_method}(new_node)" - elsif children_of_type - children_of_type.map do |child_accessor, child_class| - "node.#{child_accessor}.each do |child_node| - new_child_and_node = #{child_class.visit_method}_with_modifications(child_node, new_node) - # Reassign `node` in case the child hook makes a modification - if new_child_and_node.is_a?(Array) - new_node = new_child_and_node[1] - end - end" - end.join("\n") - else - "" - end - } - - if new_node.equal?(node) - [node, parent] - else - [new_node, parent] - end - end - end - - def #{node_method}_with_modifications(node, parent) - new_node_and_new_parent = #{node_method}(node, parent) - apply_modifications(node, parent, new_node_and_new_parent) - end - RUBY - end - def on_document_children(document_node) new_node = document_node document_node.children.each do |child_node| @@ -216,6 +161,63 @@ def on_argument_children(new_node) new_node end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + + # We don't use `alias` here because it breaks `super` + def self.make_visit_methods(ast_node_class) + node_method = ast_node_class.visit_method + children_of_type = ast_node_class.children_of_type + child_visit_method = :"#{node_method}_children" + + class_eval(<<-RUBY, __FILE__, __LINE__ + 1) + # The default implementation for visiting an AST node. + # It doesn't _do_ anything, but it continues to visiting the node's children. + # To customize this hook, override one of its make_visit_methods (or the base method?) + # in your subclasses. + # + # @param node [GraphQL::Language::Nodes::AbstractNode] the node being visited + # @param parent [GraphQL::Language::Nodes::AbstractNode, nil] the previously-visited node, or `nil` if this is the root node. + # @return [Array, nil] If there were modifications, it returns an array of new nodes, otherwise, it returns `nil`. + def #{node_method}(node, parent) + if node.equal?(DELETE_NODE) + # This might be passed to `super(DELETE_NODE, ...)` + # by a user hook, don't want to keep visiting in that case. + [node, parent] + else + new_node = node + #{ + if method_defined?(child_visit_method) + "new_node = #{child_visit_method}(new_node)" + elsif children_of_type + children_of_type.map do |child_accessor, child_class| + "node.#{child_accessor}.each do |child_node| + new_child_and_node = #{child_class.visit_method}_with_modifications(child_node, new_node) + # Reassign `node` in case the child hook makes a modification + if new_child_and_node.is_a?(Array) + new_node = new_child_and_node[1] + end + end" + end.join("\n") + else + "" + end + } + + if new_node.equal?(node) + [node, parent] + else + [new_node, parent] + end + end + end + + def #{node_method}_with_modifications(node, parent) + new_node_and_new_parent = #{node_method}(node, parent) + apply_modifications(node, parent, new_node_and_new_parent) + end + RUBY + end + [ Language::Nodes::Argument, Language::Nodes::Directive, @@ -256,6 +258,8 @@ def on_argument_children(new_node) make_visit_methods(ast_node_class) end + # rubocop:enable Development/NoEvalCop + private def apply_modifications(node, parent, new_node_and_new_parent)
lib/graphql/schema/argument.rb+3 −5 modified@@ -53,6 +53,7 @@ def from_resolver? def initialize(arg_name = nil, type_expr = nil, desc = nil, required: true, type: nil, name: nil, loads: nil, description: nil, comment: 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) arg_name ||= name @name = -(camelize ? Member::BuildType.camelize(arg_name.to_s) : arg_name.to_s) + NameValidator.validate!(@name) @type_expr = type_expr || type @description = desc || description @comment = comment @@ -89,11 +90,8 @@ def initialize(arg_name = nil, type_expr = nil, desc = nil, required: true, type end if definition_block - if definition_block.arity == 1 - instance_exec(self, &definition_block) - else - instance_eval(&definition_block) - end + # `self` will still be self, it will also be the first argument to the block: + instance_exec(self, &definition_block) end end
lib/graphql/schema/build_from_definition.rb+8 −7 modified@@ -467,17 +467,18 @@ def build_fields(owner, field_definitions, type_resolver, default_resolve:) # Don't do this for interfaces if default_resolve - owner.class_eval <<-RUBY, __FILE__, __LINE__ - # frozen_string_literal: true - def #{resolve_method_name}(**args) - field_instance = self.class.get_field("#{field_definition.name}") - context.schema.definition_default_resolve.call(self.class, field_instance, object, args, context) - end - RUBY + define_field_resolve_method(owner, resolve_method_name, field_definition.name) end end end + def define_field_resolve_method(owner, method_name, field_name) + owner.define_method(method_name) { |**args| + field_instance = self.class.get_field(field_name) + context.schema.definition_default_resolve.call(self.class, field_instance, object, args, context) + } + end + def build_resolve_type(lookup_hash, directives, missing_type_handler) resolve_type_proc = nil resolve_type_proc = ->(ast_node) {
lib/graphql/schema/directive.rb+1 −1 modified@@ -99,7 +99,7 @@ def repeatable(new_value) def inherited(subclass) super - subclass.class_eval do + subclass.class_exec do @default_graphql_name ||= nil end end
lib/graphql/schema/enum_value.rb+1 −1 modified@@ -48,7 +48,7 @@ def initialize(graphql_name, desc = nil, owner:, ast_node: nil, directives: nil, end if block_given? - instance_eval(&block) + instance_exec(self, &block) end end
lib/graphql/schema/field.rb+2 −2 modified@@ -255,7 +255,7 @@ def initialize(type: nil, name: nil, owner: nil, null: nil, description: NOT_CON @underscored_name = -Member::BuildType.underscore(name_s) @name = -(camelize ? Member::BuildType.camelize(name_s) : name_s) - + NameValidator.validate!(@name) @description = description @comment = comment @type = @owner_type = @own_validators = @own_directives = @own_arguments = @arguments_statically_coercible = nil # these will be prepared later if necessary @@ -369,7 +369,7 @@ def ensure_loaded if @definition_block.arity == 1 @definition_block.call(self) else - instance_eval(&@definition_block) + instance_exec(self, &@definition_block) end self.extensions.each(&:after_define_apply) @call_after_define = true
lib/graphql/schema/input_object.rb+6 −7 modified@@ -132,14 +132,8 @@ def argument(*args, **kwargs, &block) end end # Add a method access - method_name = argument_defn.keyword suppress_redefinition_warning do - class_eval <<-RUBY, __FILE__, __LINE__ - def #{method_name} - self[#{method_name.inspect}] - end - alias_method :#{method_name}, :#{method_name} - RUBY + define_accessor_method(argument_defn.keyword) end argument_defn end @@ -256,6 +250,11 @@ def suppress_redefinition_warning ensure $VERBOSE = verbose end + + def define_accessor_method(method_name) + define_method(method_name) { self[method_name] } + alias_method(method_name, method_name) + end end private
lib/graphql/schema/interface.rb+1 −1 modified@@ -29,7 +29,7 @@ def definition_methods(&block) const_set(:DefinitionMethods, defn_methods_module) extend(self::DefinitionMethods) end - self::DefinitionMethods.module_eval(&block) + self::DefinitionMethods.module_exec(&block) end # @see {Schema::Warden} hides interfaces without visible implementations
lib/graphql/schema/member/has_directives.rb+1 −1 modified@@ -6,7 +6,7 @@ class Member module HasDirectives def self.extended(child_cls) super - child_cls.module_eval { self.own_directives = nil } + child_cls.module_exec { self.own_directives = nil } end def inherited(child_cls)
lib/graphql/schema/member/has_fields.rb+1 −1 modified@@ -185,7 +185,7 @@ def self.extended(child_class) def inherited(subclass) super - subclass.class_eval do + subclass.class_exec do @own_fields ||= nil @field_class ||= nil end
lib/graphql/schema/member/has_interfaces.rb+1 −1 modified@@ -133,7 +133,7 @@ def self.extended(child_class) def inherited(subclass) super - subclass.class_eval do + subclass.class_exec do @own_interface_type_memberships ||= nil end end
lib/graphql/schema/member/scoped.rb+1 −1 modified@@ -30,7 +30,7 @@ def reauthorize_scoped_objects(new_value = nil) def inherited(subclass) super - subclass.class_eval do + subclass.class_exec do @reauthorize_scoped_objects = nil end end
lib/graphql/schema/member/type_system_helpers.rb+1 −1 modified@@ -42,7 +42,7 @@ def kind private def inherited(subclass) - subclass.class_eval do + subclass.class_exec do @to_non_null_type ||= nil @to_list_type ||= nil end
lib/graphql/tracing/appoptics_trace.rb+4 −0 modified@@ -28,6 +28,8 @@ def self.version Gem::Version.new('1.0.0') end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + [ 'lex', 'parse', @@ -55,6 +57,8 @@ def #{trace_method}(**data) RUBY end + # rubocop:enable Development/NoEvalCop + def execute_field(query:, field:, ast_node:, arguments:, object:) return_type = field.type.unwrap trace_field = if return_type.kind.scalar? || return_type.kind.enum?
lib/graphql/tracing/appsignal_trace.rb+4 −0 modified@@ -13,6 +13,8 @@ def initialize(set_action_name: false, **rest) super end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { "lex" => "lex.graphql", "parse" => "parse.graphql", @@ -43,6 +45,8 @@ def #{trace_method}(**data) RUBY end + # rubocop:enable Development/NoEvalCop + def platform_execute_field(platform_key) Appsignal.instrument(platform_key) do yield
lib/graphql/tracing/data_dog_trace.rb+4 −0 modified@@ -20,6 +20,8 @@ def initialize(tracer: nil, analytics_enabled: false, analytics_sample_rate: 1.0 super end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { 'lex' => 'lex.graphql', 'parse' => 'parse.graphql', @@ -69,6 +71,8 @@ def #{trace_method}(**data) RUBY end + # rubocop:enable Development/NoEvalCop + def execute_field_span(span_key, query, field, ast_node, arguments, object) return_type = field.type.unwrap trace_field = if return_type.kind.scalar? || return_type.kind.enum?
lib/graphql/tracing/notifications_trace.rb+4 −0 modified@@ -16,6 +16,8 @@ def initialize(engine:, **rest) super end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { "lex" => "lex.graphql", "parse" => "parse.graphql", @@ -39,6 +41,8 @@ def #{trace_method}(**metadata, &block) RUBY end + # rubocop:enable Development/NoEvalCop + include PlatformTrace end end
lib/graphql/tracing/platform_trace.rb+5 −0 modified@@ -39,6 +39,9 @@ def self.included(child_class) include(BaseKeyCache) } child_class.const_set(:KeyCache, key_methods_class) + + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + [:execute_field, :execute_field_lazy].each do |field_trace_method| if !child_class.method_defined?(field_trace_method) child_class.module_eval <<-RUBY, __FILE__, __LINE__ @@ -91,6 +94,8 @@ def #{rt_trace_method}(query:, type:, object:) end RUBY end + + # rubocop:enable Development/NoEvalCop end end
lib/graphql/tracing/prometheus_trace.rb+4 −0 modified@@ -13,6 +13,8 @@ def initialize(client: PrometheusExporter::Client.default, keys_whitelist: ["exe super(**rest) end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { 'lex' => "graphql.lex", 'parse' => "graphql.parse", @@ -30,6 +32,8 @@ def #{trace_method}(**data) RUBY end + # rubocop:enable Development/NoEvalCop + def platform_execute_field(platform_key, &block) instrument_prometheus_execution(platform_key, "execute_field", &block) end
lib/graphql/tracing/scout_trace.rb+3 −0 modified@@ -16,6 +16,8 @@ def initialize(set_transaction_name: false, **_rest) super end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { "lex" => "lex.graphql", "parse" => "parse.graphql", @@ -45,6 +47,7 @@ def #{trace_method}(**data) end RUBY end + # rubocop:enable Development/NoEvalCop def platform_execute_field(platform_key, &block) self.class.instrument("GraphQL", platform_key, INSTRUMENT_OPTS, &block)
lib/graphql/tracing/sentry_trace.rb+4 −0 modified@@ -23,6 +23,8 @@ def execute_query(**data) instrument_sentry_execution("graphql.execute", "execute_query", data) { super } end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { "lex" => "graphql.lex", "parse" => "graphql.parse", @@ -39,6 +41,8 @@ def #{trace_method}(**data) RUBY end + # rubocop:enable Development/NoEvalCop + def platform_execute_field(platform_key, &block) instrument_sentry_execution(platform_key, "execute_field", &block) end
lib/graphql/tracing/statsd_trace.rb+4 −0 modified@@ -11,6 +11,8 @@ def initialize(statsd:, **rest) super(**rest) end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { 'lex' => "graphql.lex", 'parse' => "graphql.parse", @@ -30,6 +32,8 @@ def #{trace_method}(**data) RUBY end + # rubocop:enable Development/NoEvalCop + def platform_execute_field(platform_key, &block) @statsd.time(platform_key, &block) end
lib/graphql/types/relay/connection_behaviors.rb+1 −1 modified@@ -13,7 +13,7 @@ def self.included(child_class) child_class.node_nullable(true) child_class.edges_nullable(true) child_class.edge_nullable(true) - child_class.module_eval { + child_class.module_exec { self.edge_type = nil self.node_type = nil self.edge_class = nil
lib/graphql/types/relay/edge_behaviors.rb+1 −1 modified@@ -8,7 +8,7 @@ def self.included(child_class) child_class.description("An edge in a connection.") child_class.field(:cursor, String, null: false, description: "A cursor for use in pagination.") child_class.extend(ClassMethods) - child_class.class_eval { self.node_type = nil } + child_class.class_exec { self.node_type = nil } child_class.node_nullable(true) child_class.default_broadcastable(nil) end
lib/graphql/version.rb+1 −1 modified@@ -1,4 +1,4 @@ # frozen_string_literal: true module GraphQL - VERSION = "2.3.20" + VERSION = "2.3.21" end
.rubocop.yml+5 −0 modified@@ -1,5 +1,6 @@ require: - ./cop/development/none_without_block_cop + - ./cop/development/no_eval_cop - ./cop/development/no_focus_cop - ./lib/graphql/rubocop/graphql/default_null_true - ./lib/graphql/rubocop/graphql/default_required_true @@ -51,6 +52,10 @@ Development/NoneWithoutBlockCop: - "lib/**/*" - "spec/**/*" +Development/NoEvalCop: + Include: + - "lib/**/*" + Development/NoFocusCop: Include: - "spec/**/*"
spec/graphql/schema/loader_spec.rb+149 −0 modified@@ -405,4 +405,153 @@ def assert_deep_equal(expected_type, actual_type) assert_equal arg.default_value, { 'id' => nil, 'int' => nil, 'float' => nil, 'enum' => nil, 'sub' => nil, 'bool' => nil, 'bigint' => nil } end end + + it "validates field argument names" do + json = { + "data" => { + "__schema" => { + "queryType" => { + "name" => "Query" + }, + "mutationType" => nil, + "subscriptionType" => nil, + "types" => [ + { + "kind" => "OBJECT", + "name" => "Query", + "description" => nil, + "fields" => [ + { + "name" => "int", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil, + }, + "args" => [ + { + "name" => "something-wrong", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil + }, + "defaultValue" => nil + } + ], + } + ] + } + ] + } + } + } + err = assert_raises GraphQL::InvalidNameError do + GraphQL::Schema.from_introspection(json) + end + + assert_includes err.message, "something-wrong" + end + + it "validates field names" do + json = { + "data" => { + "__schema" => { + "queryType" => { + "name" => "Query" + }, + "mutationType" => nil, + "subscriptionType" => nil, + "types" => [ + { + "kind" => "OBJECT", + "name" => "Query", + "description" => nil, + "fields" => [ + { + "name" => "bad.int", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil, + }, + "args" => [], + } + ] + } + ] + } + } + } + err = assert_raises GraphQL::InvalidNameError do + GraphQL::Schema.from_introspection(json) + end + + assert_includes err.message, "bad.int" + end + + it "validates input object argument names" do + json = { + "data" => { + "__schema" => { + "queryType" => { + "name" => "Query" + }, + "mutationType" => nil, + "subscriptionType" => nil, + "types" => [ + { + "kind" => "OBJECT", + "name" => "Query", + "description" => nil, + "fields" => [ + { + "name" => "int", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil, + }, + "args" => [ + { + "name" => "inputObject", + "description" => nil, + "type" => { + "kind" => "INPUT_OBJECT", + "name" => "SomeInputObject", + "ofType" => nil + }, + "defaultValue" => nil + } + ], + } + ] + }, + { + "kind" => "INPUT_OBJECT", + "name" => "SomeInputObject", + "description" => nil, + "inputFields" => [ + { + "name"=>"bad, input", + "type"=> { "kind" => "SCALAR", "name" => "String"}, + "defaultValue"=> nil, + "description" => nil, + }, + ] + } + ] + } + } + } + err = assert_raises GraphQL::InvalidNameError do + GraphQL::Schema.from_introspection(json) + end + + assert_includes err.message, "bad, input" + end end
e58676c70aa6Merge commit from fork
33 files changed · +350 −125
benchmark/run.rb+7 −2 modified@@ -112,12 +112,16 @@ def self.build_large_schema end obj_ts = 100.times.map do |n| + input_obj_t = Class.new(GraphQL::Schema::InputObject) do + graphql_name("Input#{n}") + argument :arg, String + end obj_t = Class.new(GraphQL::Schema::Object) do graphql_name("Object#{n}") implements(*int_ts) 20.times do |n2| field :"field#{n2}", String do - argument :arg, String + argument :input, input_obj_t end end @@ -154,8 +158,9 @@ def self.profile_boot end StackProf::Report.new(result).print_text + retained_schema = nil report = MemoryProfiler.report do - build_large_schema + retained_schema = build_large_schema end report.pretty_print
cop/development/no_eval_cop.rb+18 −0 added@@ -0,0 +1,18 @@ +# frozen_string_literal: true +require 'rubocop' + +module Cop + module Development + class NoEvalCop < RuboCop::Cop::Base + 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." + + def on_send(node) + case node.method_name + when :module_eval, :class_eval, :instance_eval + message = MSG_TEMPLATE % { eval_method_name: node.method_name, exec_method_name: node.method_name.to_s.sub("eval", "exec").to_sym } + add_offense node, message: message + end + end + end + end +end
lib/graphql/analysis/analyzer.rb+2 −1 modified@@ -42,6 +42,7 @@ def result raise GraphQL::RequiredImplementationMissingError end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time class << self private @@ -72,7 +73,7 @@ def on_leave_#{member_name}(node, parent, visitor) build_visitor_hooks :variable_definition build_visitor_hooks :variable_identifier build_visitor_hooks :abstract_node - + # rubocop:enable Development/NoEvalCop protected # @return [GraphQL::Query, GraphQL::Execution::Multiplex] Whatever this analyzer is analyzing
lib/graphql/analysis/visitor.rb+2 −0 modified@@ -69,6 +69,7 @@ def response_path @response_path.dup end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time # Visitor Hooks [ :operation_definition, :fragment_definition, @@ -93,6 +94,7 @@ def call_on_leave_#{node_type}(node, parent) RUBY end + # rubocop:enable Development/NoEvalCop def on_operation_definition(node, parent) check_timeout
lib/graphql/language/nodes.rb+3 −0 modified@@ -141,6 +141,8 @@ def merge!(new_options) end class << self + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + # Add a default `#visit_method` and `#children_method_name` using the class name def inherited(child_class) super @@ -343,6 +345,7 @@ def marshal_load(values) RUBY end end + # rubocop:enable Development/NoEvalCop end end
lib/graphql/language/static_visitor.rb+37 −33 modified@@ -22,39 +22,6 @@ def visit end end - # We don't use `alias` here because it breaks `super` - def self.make_visit_methods(ast_node_class) - node_method = ast_node_class.visit_method - children_of_type = ast_node_class.children_of_type - child_visit_method = :"#{node_method}_children" - - class_eval(<<-RUBY, __FILE__, __LINE__ + 1) - # The default implementation for visiting an AST node. - # It doesn't _do_ anything, but it continues to visiting the node's children. - # To customize this hook, override one of its make_visit_methods (or the base method?) - # in your subclasses. - # - # @param node [GraphQL::Language::Nodes::AbstractNode] the node being visited - # @param parent [GraphQL::Language::Nodes::AbstractNode, nil] the previously-visited node, or `nil` if this is the root node. - # @return [void] - def #{node_method}(node, parent) - #{ - if method_defined?(child_visit_method) - "#{child_visit_method}(node)" - elsif children_of_type - children_of_type.map do |child_accessor, child_class| - "node.#{child_accessor}.each do |child_node| - #{child_class.visit_method}(child_node, node) - end" - end.join("\n") - else - "" - end - } - end - RUBY - end - def on_document_children(document_node) document_node.children.each do |child_node| visit_method = child_node.visit_method @@ -123,6 +90,41 @@ def on_argument_children(new_node) end end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + + # We don't use `alias` here because it breaks `super` + def self.make_visit_methods(ast_node_class) + node_method = ast_node_class.visit_method + children_of_type = ast_node_class.children_of_type + child_visit_method = :"#{node_method}_children" + + class_eval(<<-RUBY, __FILE__, __LINE__ + 1) + # The default implementation for visiting an AST node. + # It doesn't _do_ anything, but it continues to visiting the node's children. + # To customize this hook, override one of its make_visit_methods (or the base method?) + # in your subclasses. + # + # @param node [GraphQL::Language::Nodes::AbstractNode] the node being visited + # @param parent [GraphQL::Language::Nodes::AbstractNode, nil] the previously-visited node, or `nil` if this is the root node. + # @return [void] + def #{node_method}(node, parent) + #{ + if method_defined?(child_visit_method) + "#{child_visit_method}(node)" + elsif children_of_type + children_of_type.map do |child_accessor, child_class| + "node.#{child_accessor}.each do |child_node| + #{child_class.visit_method}(child_node, node) + end" + end.join("\n") + else + "" + end + } + end + RUBY + end + [ Language::Nodes::Argument, Language::Nodes::Directive, @@ -162,6 +164,8 @@ def on_argument_children(new_node) ].each do |ast_node_class| make_visit_methods(ast_node_class) end + + # rubocop:disable Development/NoEvalCop end end end
lib/graphql/language/visitor.rb+59 −55 modified@@ -61,61 +61,6 @@ def visit end end - # We don't use `alias` here because it breaks `super` - def self.make_visit_methods(ast_node_class) - node_method = ast_node_class.visit_method - children_of_type = ast_node_class.children_of_type - child_visit_method = :"#{node_method}_children" - - class_eval(<<-RUBY, __FILE__, __LINE__ + 1) - # The default implementation for visiting an AST node. - # It doesn't _do_ anything, but it continues to visiting the node's children. - # To customize this hook, override one of its make_visit_methods (or the base method?) - # in your subclasses. - # - # @param node [GraphQL::Language::Nodes::AbstractNode] the node being visited - # @param parent [GraphQL::Language::Nodes::AbstractNode, nil] the previously-visited node, or `nil` if this is the root node. - # @return [Array, nil] If there were modifications, it returns an array of new nodes, otherwise, it returns `nil`. - def #{node_method}(node, parent) - if node.equal?(DELETE_NODE) - # This might be passed to `super(DELETE_NODE, ...)` - # by a user hook, don't want to keep visiting in that case. - [node, parent] - else - new_node = node - #{ - if method_defined?(child_visit_method) - "new_node = #{child_visit_method}(new_node)" - elsif children_of_type - children_of_type.map do |child_accessor, child_class| - "node.#{child_accessor}.each do |child_node| - new_child_and_node = #{child_class.visit_method}_with_modifications(child_node, new_node) - # Reassign `node` in case the child hook makes a modification - if new_child_and_node.is_a?(Array) - new_node = new_child_and_node[1] - end - end" - end.join("\n") - else - "" - end - } - - if new_node.equal?(node) - [node, parent] - else - [new_node, parent] - end - end - end - - def #{node_method}_with_modifications(node, parent) - new_node_and_new_parent = #{node_method}(node, parent) - apply_modifications(node, parent, new_node_and_new_parent) - end - RUBY - end - def on_document_children(document_node) new_node = document_node document_node.children.each do |child_node| @@ -216,6 +161,63 @@ def on_argument_children(new_node) new_node end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + + # We don't use `alias` here because it breaks `super` + def self.make_visit_methods(ast_node_class) + node_method = ast_node_class.visit_method + children_of_type = ast_node_class.children_of_type + child_visit_method = :"#{node_method}_children" + + class_eval(<<-RUBY, __FILE__, __LINE__ + 1) + # The default implementation for visiting an AST node. + # It doesn't _do_ anything, but it continues to visiting the node's children. + # To customize this hook, override one of its make_visit_methods (or the base method?) + # in your subclasses. + # + # @param node [GraphQL::Language::Nodes::AbstractNode] the node being visited + # @param parent [GraphQL::Language::Nodes::AbstractNode, nil] the previously-visited node, or `nil` if this is the root node. + # @return [Array, nil] If there were modifications, it returns an array of new nodes, otherwise, it returns `nil`. + def #{node_method}(node, parent) + if node.equal?(DELETE_NODE) + # This might be passed to `super(DELETE_NODE, ...)` + # by a user hook, don't want to keep visiting in that case. + [node, parent] + else + new_node = node + #{ + if method_defined?(child_visit_method) + "new_node = #{child_visit_method}(new_node)" + elsif children_of_type + children_of_type.map do |child_accessor, child_class| + "node.#{child_accessor}.each do |child_node| + new_child_and_node = #{child_class.visit_method}_with_modifications(child_node, new_node) + # Reassign `node` in case the child hook makes a modification + if new_child_and_node.is_a?(Array) + new_node = new_child_and_node[1] + end + end" + end.join("\n") + else + "" + end + } + + if new_node.equal?(node) + [node, parent] + else + [new_node, parent] + end + end + end + + def #{node_method}_with_modifications(node, parent) + new_node_and_new_parent = #{node_method}(node, parent) + apply_modifications(node, parent, new_node_and_new_parent) + end + RUBY + end + [ Language::Nodes::Argument, Language::Nodes::Directive, @@ -256,6 +258,8 @@ def on_argument_children(new_node) make_visit_methods(ast_node_class) end + # rubocop:enable Development/NoEvalCop + private def apply_modifications(node, parent, new_node_and_new_parent)
lib/graphql/schema/argument.rb+3 −5 modified@@ -53,6 +53,7 @@ def from_resolver? def initialize(arg_name = nil, type_expr = nil, desc = nil, required: true, type: nil, name: nil, loads: nil, description: nil, comment: 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) arg_name ||= name @name = -(camelize ? Member::BuildType.camelize(arg_name.to_s) : arg_name.to_s) + NameValidator.validate!(@name) @type_expr = type_expr || type @description = desc || description @comment = comment @@ -89,11 +90,8 @@ def initialize(arg_name = nil, type_expr = nil, desc = nil, required: true, type end if definition_block - if definition_block.arity == 1 - instance_exec(self, &definition_block) - else - instance_eval(&definition_block) - end + # `self` will still be self, it will also be the first argument to the block: + instance_exec(self, &definition_block) end end
lib/graphql/schema/build_from_definition.rb+8 −7 modified@@ -467,17 +467,18 @@ def build_fields(owner, field_definitions, type_resolver, default_resolve:) # Don't do this for interfaces if default_resolve - owner.class_eval <<-RUBY, __FILE__, __LINE__ - # frozen_string_literal: true - def #{resolve_method_name}(**args) - field_instance = self.class.get_field("#{field_definition.name}") - context.schema.definition_default_resolve.call(self.class, field_instance, object, args, context) - end - RUBY + define_field_resolve_method(owner, resolve_method_name, field_definition.name) end end end + def define_field_resolve_method(owner, method_name, field_name) + owner.define_method(method_name) { |**args| + field_instance = self.class.get_field(field_name) + context.schema.definition_default_resolve.call(self.class, field_instance, object, args, context) + } + end + def build_resolve_type(lookup_hash, directives, missing_type_handler) resolve_type_proc = nil resolve_type_proc = ->(ast_node) {
lib/graphql/schema/directive.rb+1 −1 modified@@ -99,7 +99,7 @@ def repeatable(new_value) def inherited(subclass) super - subclass.class_eval do + subclass.class_exec do @default_graphql_name ||= nil end end
lib/graphql/schema/enum.rb+2 −2 modified@@ -234,7 +234,7 @@ def inherited(child_class) # because they would end up with names like `#<Class0x1234>::UnresolvedValueError` which messes up bug trackers child_class.const_set(:UnresolvedValueError, Class.new(Schema::Enum::UnresolvedValueError)) end - child_class.class_eval { @value_methods = nil } + child_class.class_exec { @value_methods = nil } super end @@ -256,7 +256,7 @@ def generate_value_method(value, configured_value_method) return end - instance_eval("def #{value_method_name}; #{value.graphql_name.inspect}; end;", __FILE__, __LINE__) + define_singleton_method(value_method_name) { value.graphql_name } end end
lib/graphql/schema/enum_value.rb+1 −1 modified@@ -48,7 +48,7 @@ def initialize(graphql_name, desc = nil, owner:, ast_node: nil, directives: nil, end if block_given? - instance_eval(&block) + instance_exec(self, &block) end end
lib/graphql/schema/field.rb+2 −2 modified@@ -255,7 +255,7 @@ def initialize(type: nil, name: nil, owner: nil, null: nil, description: NOT_CON @underscored_name = -Member::BuildType.underscore(name_s) @name = -(camelize ? Member::BuildType.camelize(name_s) : name_s) - + NameValidator.validate!(@name) @description = description @comment = comment @type = @owner_type = @own_validators = @own_directives = @own_arguments = @arguments_statically_coercible = nil # these will be prepared later if necessary @@ -369,7 +369,7 @@ def ensure_loaded if @definition_block.arity == 1 @definition_block.call(self) else - instance_eval(&@definition_block) + instance_exec(self, &@definition_block) end self.extensions.each(&:after_define_apply) @call_after_define = true
lib/graphql/schema/input_object.rb+7 −8 modified@@ -59,7 +59,7 @@ def deconstruct_keys(keys = nil) else new_h = {} keys.each { |k| @ruby_style_hash.key?(k) && new_h[k] = @ruby_style_hash[k] } - new_h + new_h end end @@ -150,14 +150,8 @@ def argument(*args, **kwargs, &block) end end # Add a method access - method_name = argument_defn.keyword suppress_redefinition_warning do - class_eval <<-RUBY, __FILE__, __LINE__ - def #{method_name} - self[#{method_name.inspect}] - end - alias_method #{method_name.inspect}, #{method_name.inspect} - RUBY + define_accessor_method(argument_defn.keyword) end argument_defn end @@ -293,6 +287,11 @@ def suppress_redefinition_warning ensure $VERBOSE = verbose end + + def define_accessor_method(method_name) + define_method(method_name) { self[method_name] } + alias_method(method_name, method_name) + end end private
lib/graphql/schema/interface.rb+1 −1 modified@@ -30,7 +30,7 @@ def definition_methods(&block) const_set(:DefinitionMethods, defn_methods_module) extend(self::DefinitionMethods) end - self::DefinitionMethods.module_eval(&block) + self::DefinitionMethods.module_exec(&block) end # @see {Schema::Warden} hides interfaces without visible implementations
lib/graphql/schema/member/has_directives.rb+1 −1 modified@@ -6,7 +6,7 @@ class Member module HasDirectives def self.extended(child_cls) super - child_cls.module_eval { self.own_directives = nil } + child_cls.module_exec { self.own_directives = nil } end def inherited(child_cls)
lib/graphql/schema/member/has_fields.rb+1 −1 modified@@ -202,7 +202,7 @@ def self.extended(child_class) def inherited(subclass) super - subclass.class_eval do + subclass.class_exec do @own_fields ||= nil @field_class ||= nil @has_no_fields ||= false
lib/graphql/schema/member/has_interfaces.rb+1 −1 modified@@ -133,7 +133,7 @@ def self.extended(child_class) def inherited(subclass) super - subclass.class_eval do + subclass.class_exec do @own_interface_type_memberships ||= nil end end
lib/graphql/schema/member/scoped.rb+1 −1 modified@@ -30,7 +30,7 @@ def reauthorize_scoped_objects(new_value = nil) def inherited(subclass) super - subclass.class_eval do + subclass.class_exec do @reauthorize_scoped_objects = nil end end
lib/graphql/schema/member/type_system_helpers.rb+1 −1 modified@@ -42,7 +42,7 @@ def kind private def inherited(subclass) - subclass.class_eval do + subclass.class_exec do @to_non_null_type ||= nil @to_list_type ||= nil end
lib/graphql/tracing/appoptics_trace.rb+4 −0 modified@@ -32,6 +32,8 @@ def self.version Gem::Version.new('1.0.0') end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + [ 'lex', 'parse', @@ -59,6 +61,8 @@ def #{trace_method}(**data) RUBY end + # rubocop:enable Development/NoEvalCop + def execute_field(query:, field:, ast_node:, arguments:, object:) return_type = field.type.unwrap trace_field = if return_type.kind.scalar? || return_type.kind.enum?
lib/graphql/tracing/appsignal_trace.rb+4 −0 modified@@ -21,6 +21,8 @@ def initialize(set_action_name: false, **rest) super end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { "lex" => "lex.graphql", "parse" => "parse.graphql", @@ -51,6 +53,8 @@ def #{trace_method}(**data) RUBY end + # rubocop:enable Development/NoEvalCop + def platform_execute_field(platform_key) Appsignal.instrument(platform_key) do yield
lib/graphql/tracing/data_dog_trace.rb+4 −0 modified@@ -27,6 +27,8 @@ def initialize(tracer: nil, analytics_enabled: false, analytics_sample_rate: 1.0 super end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { 'lex' => 'lex.graphql', 'parse' => 'parse.graphql', @@ -76,6 +78,8 @@ def #{trace_method}(**data) RUBY end + # rubocop:enable Development/NoEvalCop + def execute_field_span(span_key, query, field, ast_node, arguments, object) return_type = field.type.unwrap trace_field = if return_type.kind.scalar? || return_type.kind.enum?
lib/graphql/tracing/notifications_trace.rb+4 −0 modified@@ -16,6 +16,8 @@ def initialize(engine:, **rest) super end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { "lex" => "lex.graphql", "parse" => "parse.graphql", @@ -39,6 +41,8 @@ def #{trace_method}(**metadata, &block) RUBY end + # rubocop:enable Development/NoEvalCop + include PlatformTrace end end
lib/graphql/tracing/platform_trace.rb+5 −0 modified@@ -39,6 +39,9 @@ def self.included(child_class) include(BaseKeyCache) } child_class.const_set(:KeyCache, key_methods_class) + + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + [:execute_field, :execute_field_lazy].each do |field_trace_method| if !child_class.method_defined?(field_trace_method) child_class.module_eval <<-RUBY, __FILE__, __LINE__ @@ -91,6 +94,8 @@ def #{rt_trace_method}(query:, type:, object:) end RUBY end + + # rubocop:enable Development/NoEvalCop end end
lib/graphql/tracing/prometheus_trace.rb+4 −0 modified@@ -40,6 +40,8 @@ def initialize(client: PrometheusExporter::Client.default, keys_whitelist: ["exe super(**rest) end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { 'lex' => "graphql.lex", 'parse' => "graphql.parse", @@ -57,6 +59,8 @@ def #{trace_method}(**data) RUBY end + # rubocop:enable Development/NoEvalCop + def platform_execute_field(platform_key, &block) instrument_prometheus_execution(platform_key, "execute_field", &block) end
lib/graphql/tracing/scout_trace.rb+3 −0 modified@@ -24,6 +24,8 @@ def initialize(set_transaction_name: false, **_rest) super end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { "lex" => "lex.graphql", "parse" => "parse.graphql", @@ -53,6 +55,7 @@ def #{trace_method}(**data) end RUBY end + # rubocop:enable Development/NoEvalCop def platform_execute_field(platform_key, &block) self.class.instrument("GraphQL", platform_key, INSTRUMENT_OPTS, &block)
lib/graphql/tracing/sentry_trace.rb+4 −0 modified@@ -30,6 +30,8 @@ def execute_query(**data) instrument_sentry_execution("graphql.execute", "execute_query", data) { super } end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { "lex" => "graphql.lex", "parse" => "graphql.parse", @@ -46,6 +48,8 @@ def #{trace_method}(**data) RUBY end + # rubocop:enable Development/NoEvalCop + def platform_execute_field(platform_key, &block) instrument_sentry_execution(platform_key, "execute_field", &block) end
lib/graphql/tracing/statsd_trace.rb+4 −0 modified@@ -22,6 +22,8 @@ def initialize(statsd:, **rest) super(**rest) end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { 'lex' => "graphql.lex", 'parse' => "graphql.parse", @@ -41,6 +43,8 @@ def #{trace_method}(**data) RUBY end + # rubocop:enable Development/NoEvalCop + def platform_execute_field(platform_key, &block) @statsd.time(platform_key, &block) end
lib/graphql/types/relay/connection_behaviors.rb+1 −1 modified@@ -13,7 +13,7 @@ def self.included(child_class) child_class.node_nullable(true) child_class.edges_nullable(true) child_class.edge_nullable(true) - child_class.module_eval { + child_class.module_exec { self.edge_type = nil self.node_type = nil self.edge_class = nil
lib/graphql/types/relay/edge_behaviors.rb+1 −1 modified@@ -8,7 +8,7 @@ def self.included(child_class) child_class.description("An edge in a connection.") child_class.field(:cursor, String, null: false, description: "A cursor for use in pagination.") child_class.extend(ClassMethods) - child_class.class_eval { self.node_type = nil } + child_class.class_exec { self.node_type = nil } child_class.node_nullable(true) child_class.default_broadcastable(nil) end
.rubocop.yml+5 −0 modified@@ -1,5 +1,6 @@ require: - ./cop/development/none_without_block_cop + - ./cop/development/no_eval_cop - ./cop/development/no_focus_cop - ./lib/graphql/rubocop/graphql/default_null_true - ./lib/graphql/rubocop/graphql/default_required_true @@ -41,6 +42,10 @@ Development/NoneWithoutBlockCop: - "lib/**/*" - "spec/**/*" +Development/NoEvalCop: + Include: + - "lib/**/*" + Development/NoFocusCop: Include: - "spec/**/*"
spec/graphql/schema/loader_spec.rb+149 −0 modified@@ -408,4 +408,153 @@ def assert_deep_equal(expected_type, actual_type) assert_equal arg.default_value, { 'id' => nil, 'int' => nil, 'float' => nil, 'enum' => nil, 'sub' => nil, 'bool' => nil, 'bigint' => nil } end end + + it "validates field argument names" do + json = { + "data" => { + "__schema" => { + "queryType" => { + "name" => "Query" + }, + "mutationType" => nil, + "subscriptionType" => nil, + "types" => [ + { + "kind" => "OBJECT", + "name" => "Query", + "description" => nil, + "fields" => [ + { + "name" => "int", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil, + }, + "args" => [ + { + "name" => "something-wrong", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil + }, + "defaultValue" => nil + } + ], + } + ] + } + ] + } + } + } + err = assert_raises GraphQL::InvalidNameError do + GraphQL::Schema.from_introspection(json) + end + + assert_includes err.message, "something-wrong" + end + + it "validates field names" do + json = { + "data" => { + "__schema" => { + "queryType" => { + "name" => "Query" + }, + "mutationType" => nil, + "subscriptionType" => nil, + "types" => [ + { + "kind" => "OBJECT", + "name" => "Query", + "description" => nil, + "fields" => [ + { + "name" => "bad.int", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil, + }, + "args" => [], + } + ] + } + ] + } + } + } + err = assert_raises GraphQL::InvalidNameError do + GraphQL::Schema.from_introspection(json) + end + + assert_includes err.message, "bad.int" + end + + it "validates input object argument names" do + json = { + "data" => { + "__schema" => { + "queryType" => { + "name" => "Query" + }, + "mutationType" => nil, + "subscriptionType" => nil, + "types" => [ + { + "kind" => "OBJECT", + "name" => "Query", + "description" => nil, + "fields" => [ + { + "name" => "int", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil, + }, + "args" => [ + { + "name" => "inputObject", + "description" => nil, + "type" => { + "kind" => "INPUT_OBJECT", + "name" => "SomeInputObject", + "ofType" => nil + }, + "defaultValue" => nil + } + ], + } + ] + }, + { + "kind" => "INPUT_OBJECT", + "name" => "SomeInputObject", + "description" => nil, + "inputFields" => [ + { + "name"=>"bad, input", + "type"=> { "kind" => "SCALAR", "name" => "String"}, + "defaultValue"=> nil, + "description" => nil, + }, + ] + } + ] + } + } + } + err = assert_raises GraphQL::InvalidNameError do + GraphQL::Schema.from_introspection(json) + end + + assert_includes err.message, "bad, input" + end end
2d2f4ed1f794Merge commit from fork
10 files changed · +196 −20
cop/development/no_eval_cop.rb+18 −0 added@@ -0,0 +1,18 @@ +# frozen_string_literal: true +require 'rubocop' + +module Cop + module Development + class NoEvalCop < RuboCop::Cop::Base + MSG_TEMPLATE = "Don't use `%{eval_method_name}` which accept strings and may result in unexpected code being generated. Use `%{exec_method_name}` instead, and pass a block." + + def on_send(node) + case node.method_name + when :module_eval, :class_eval, :instance_eval + message = MSG_TEMPLATE % { eval_method_name: node.method_name, exec_method_name: node.method_name.to_s.sub("eval", "exec").to_sym } + add_offense node, message: message + end + end + end + end +end
lib/graphql/language/nodes.rb+5 −0 modified@@ -133,6 +133,8 @@ def merge!(new_options) end class << self + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + # Add a default `#visit_method` and `#children_method_name` using the class name def inherited(child_class) super @@ -265,8 +267,11 @@ def initialize_node #{arguments.join(", ")} #{assignments.join("\n")} end RUBY + + # rubocop:enable Development/NoEvalCop end end + # rubocop:enable Development/NoEvalCop end end
lib/graphql/language/visitor.rb+1 −0 modified@@ -73,6 +73,7 @@ def visit @document end end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time # Call the user-defined handler for `node`. def visit_node(node, parent)
lib/graphql/schema/argument.rb+3 −5 modified@@ -49,6 +49,7 @@ def from_resolver? def initialize(arg_name = nil, type_expr = nil, desc = nil, required:, type: nil, name: nil, loads: nil, description: nil, ast_node: nil, default_value: NO_DEFAULT, as: nil, from_resolver: false, camelize: true, prepare: nil, method_access: true, owner:, deprecation_reason: nil, &definition_block) arg_name ||= name @name = -(camelize ? Member::BuildType.camelize(arg_name.to_s) : arg_name.to_s) + NameValidator.validate!(@name) @type_expr = type_expr || type @description = desc || description @null = !required @@ -64,11 +65,8 @@ def initialize(arg_name = nil, type_expr = nil, desc = nil, required:, type: nil self.deprecation_reason = deprecation_reason if definition_block - if definition_block.arity == 1 - instance_exec(self, &definition_block) - else - instance_eval(&definition_block) - end + # `self` will still be self, it will also be the first argument to the block: + instance_exec(self, &definition_block) end end
lib/graphql/schema/build_from_definition.rb+8 −7 modified@@ -342,13 +342,7 @@ def build_fields(owner, field_definitions, type_resolver, default_resolve:) # Don't do this for interfaces if default_resolve - owner.class_eval <<-RUBY, __FILE__, __LINE__ - # frozen_string_literal: true - def #{resolve_method_name}(**args) - field_instance = self.class.get_field("#{field_definition.name}") - context.schema.definition_default_resolve.call(self.class, field_instance, object, args, context) - end - RUBY + define_field_resolve_method(owner, resolve_method_name, field_definition.name) end end end @@ -367,6 +361,13 @@ def resolve_type(types, ast_node) end end + def define_field_resolve_method(owner, method_name, field_name) + owner.define_method(method_name) { |**args| + field_instance = self.class.get_field(field_name) + context.schema.definition_default_resolve.call(self.class, field_instance, object, args, context) + } + end + def resolve_type_name(type) case type when GraphQL::Language::Nodes::TypeName
lib/graphql/schema/enum_value.rb+1 −1 modified@@ -49,7 +49,7 @@ def initialize(graphql_name, desc = nil, owner:, ast_node: nil, description: nil @ast_node = ast_node if block_given? - instance_eval(&block) + instance_exec(self, &block) end end
lib/graphql/schema/field.rb+1 −0 modified@@ -222,6 +222,7 @@ def initialize(type: nil, name: nil, owner: nil, null: nil, field: nil, function name_s = -name.to_s @underscored_name = -Member::BuildType.underscore(name_s) @name = -(camelize ? Member::BuildType.camelize(name_s) : name_s) + NameValidator.validate!(@name) @description = description if field.is_a?(GraphQL::Schema::Field) raise ArgumentError, "Instead of passing a field as `field:`, use `add_field(field)` to add an already-defined field."
lib/graphql/schema/input_object.rb+9 −6 modified@@ -125,12 +125,8 @@ class << self def argument(*args, **kwargs, &block) argument_defn = super(*args, **kwargs, &block) # Add a method access - method_name = argument_defn.keyword - class_eval <<-RUBY, __FILE__, __LINE__ - def #{method_name} - self[#{method_name.inspect}] - end - RUBY + define_accessor_method(argument_defn.keyword) + argument_defn end def to_graphql @@ -239,6 +235,13 @@ def coerce_result(value, ctx) result end + + private + + def define_accessor_method(method_name) + define_method(method_name) { self[method_name] } + alias_method(method_name, method_name) + end end end end
lib/graphql/version.rb+1 −1 modified@@ -1,4 +1,4 @@ # frozen_string_literal: true module GraphQL - VERSION = "1.11.7" + VERSION = "1.11.8" end
spec/graphql/schema/loader_spec.rb+149 −0 modified@@ -389,4 +389,153 @@ def assert_deep_equal(expected_type, actual_type) assert_equal arg.default_value, { 'id' => nil, 'int' => nil, 'float' => nil, 'enum' => nil, 'sub' => nil, 'bool' => nil, 'bigint' => nil } end end + + it "validates field argument names" do + json = { + "data" => { + "__schema" => { + "queryType" => { + "name" => "Query" + }, + "mutationType" => nil, + "subscriptionType" => nil, + "types" => [ + { + "kind" => "OBJECT", + "name" => "Query", + "description" => nil, + "fields" => [ + { + "name" => "int", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil, + }, + "args" => [ + { + "name" => "something-wrong", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil + }, + "defaultValue" => nil + } + ], + } + ] + } + ] + } + } + } + err = assert_raises GraphQL::InvalidNameError do + GraphQL::Schema.from_introspection(json) + end + + assert_includes err.message, "something-wrong" + end + + it "validates field names" do + json = { + "data" => { + "__schema" => { + "queryType" => { + "name" => "Query" + }, + "mutationType" => nil, + "subscriptionType" => nil, + "types" => [ + { + "kind" => "OBJECT", + "name" => "Query", + "description" => nil, + "fields" => [ + { + "name" => "bad.int", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil, + }, + "args" => [], + } + ] + } + ] + } + } + } + err = assert_raises GraphQL::InvalidNameError do + GraphQL::Schema.from_introspection(json) + end + + assert_includes err.message, "bad.int" + end + + it "validates input object argument names" do + json = { + "data" => { + "__schema" => { + "queryType" => { + "name" => "Query" + }, + "mutationType" => nil, + "subscriptionType" => nil, + "types" => [ + { + "kind" => "OBJECT", + "name" => "Query", + "description" => nil, + "fields" => [ + { + "name" => "int", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil, + }, + "args" => [ + { + "name" => "inputObject", + "description" => nil, + "type" => { + "kind" => "INPUT_OBJECT", + "name" => "SomeInputObject", + "ofType" => nil + }, + "defaultValue" => nil + } + ], + } + ] + }, + { + "kind" => "INPUT_OBJECT", + "name" => "SomeInputObject", + "description" => nil, + "inputFields" => [ + { + "name"=>"bad, input", + "type"=> { "kind" => "SCALAR", "name" => "String"}, + "defaultValue"=> nil, + "description" => nil, + }, + ] + } + ] + } + } + } + err = assert_raises GraphQL::InvalidNameError do + GraphQL::Schema.from_introspection(json) + end + + assert_includes err.message, "bad, input" + end end
d0963289e0daMerge commit from fork
27 files changed · +307 −91
benchmark/run.rb+7 −2 modified@@ -111,12 +111,16 @@ def self.build_large_schema end obj_ts = 100.times.map do |n| + input_obj_t = Class.new(GraphQL::Schema::InputObject) do + graphql_name("Input#{n}") + argument :arg, String + end obj_t = Class.new(GraphQL::Schema::Object) do graphql_name("Object#{n}") implements(*int_ts) 20.times do |n2| field :"field#{n2}", String do - argument :arg, String + argument :input, input_obj_t end end @@ -153,8 +157,9 @@ def self.profile_boot end StackProf::Report.new(result).print_text + retained_schema = nil report = MemoryProfiler.report do - build_large_schema + retained_schema = build_large_schema end report.pretty_print
cop/development/no_eval_cop.rb+18 −0 added@@ -0,0 +1,18 @@ +# frozen_string_literal: true +require 'rubocop' + +module Cop + module Development + class NoEvalCop < RuboCop::Cop::Base + 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." + + def on_send(node) + case node.method_name + when :module_eval, :class_eval, :instance_eval + message = MSG_TEMPLATE % { eval_method_name: node.method_name, exec_method_name: node.method_name.to_s.sub("eval", "exec").to_sym } + add_offense node, message: message + end + end + end + end +end
lib/graphql/language/nodes.rb+3 −0 modified@@ -138,6 +138,8 @@ def merge!(new_options) end class << self + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + # Add a default `#visit_method` and `#children_method_name` using the class name def inherited(child_class) super @@ -296,6 +298,7 @@ def self.from_a(filename, line, col, #{(scalar_method_names + @children_methods. RUBY end end + # rubocop:enable Development/NoEvalCop end end
lib/graphql/language/visitor.rb+64 −61 modified@@ -76,67 +76,6 @@ def visit end end - # We don't use `alias` here because it breaks `super` - def self.make_visit_methods(ast_node_class) - node_method = ast_node_class.visit_method - children_of_type = ast_node_class.children_of_type - child_visit_method = :"#{node_method}_children" - - class_eval(<<-RUBY, __FILE__, __LINE__ + 1) - # The default implementation for visiting an AST node. - # It doesn't _do_ anything, but it continues to visiting the node's children. - # To customize this hook, override one of its make_visit_methods (or the base method?) - # in your subclasses. - # - # For compatibility, it calls hook procs, too. - # @param node [GraphQL::Language::Nodes::AbstractNode] the node being visited - # @param parent [GraphQL::Language::Nodes::AbstractNode, nil] the previously-visited node, or `nil` if this is the root node. - # @return [Array, nil] If there were modifications, it returns an array of new nodes, otherwise, it returns `nil`. - def #{node_method}(node, parent) - if node.equal?(DELETE_NODE) - # This might be passed to `super(DELETE_NODE, ...)` - # by a user hook, don't want to keep visiting in that case. - [node, parent] - else - # Run hooks if there are any - new_node = node - no_hooks = !@visitors.key?(node.class) - if no_hooks || begin_visit(new_node, parent) - #{ - if method_defined?(child_visit_method) - "new_node = #{child_visit_method}(new_node)" - elsif children_of_type - children_of_type.map do |child_accessor, child_class| - "node.#{child_accessor}.each do |child_node| - new_child_and_node = #{child_class.visit_method}_with_modifications(child_node, new_node) - # Reassign `node` in case the child hook makes a modification - if new_child_and_node.is_a?(Array) - new_node = new_child_and_node[1] - end - end" - end.join("\n") - else - "" - end - } - end - end_visit(new_node, parent) unless no_hooks - - if new_node.equal?(node) - [node, parent] - else - [new_node, parent] - end - end - end - - def #{node_method}_with_modifications(node, parent) - new_node_and_new_parent = #{node_method}(node, parent) - apply_modifications(node, parent, new_node_and_new_parent) - end - RUBY - end - def on_document_children(document_node) new_node = document_node document_node.children.each do |child_node| @@ -237,6 +176,68 @@ def on_argument_children(new_node) new_node end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + + def self.make_visit_methods(ast_node_class) + node_method = ast_node_class.visit_method + children_of_type = ast_node_class.children_of_type + child_visit_method = :"#{node_method}_children" + + class_eval(<<-RUBY, __FILE__, __LINE__ + 1) + # The default implementation for visiting an AST node. + # It doesn't _do_ anything, but it continues to visiting the node's children. + # To customize this hook, override one of its make_visit_methods (or the base method?) + # in your subclasses. + # + # For compatibility, it calls hook procs, too. + # @param node [GraphQL::Language::Nodes::AbstractNode] the node being visited + # @param parent [GraphQL::Language::Nodes::AbstractNode, nil] the previously-visited node, or `nil` if this is the root node. + # @return [Array, nil] If there were modifications, it returns an array of new nodes, otherwise, it returns `nil`. + def #{node_method}(node, parent) + if node.equal?(DELETE_NODE) + # This might be passed to `super(DELETE_NODE, ...)` + # by a user hook, don't want to keep visiting in that case. + [node, parent] + else + # Run hooks if there are any + new_node = node + no_hooks = !@visitors.key?(node.class) + if no_hooks || begin_visit(new_node, parent) + #{ + if method_defined?(child_visit_method) + "new_node = #{child_visit_method}(new_node)" + elsif children_of_type + children_of_type.map do |child_accessor, child_class| + "node.#{child_accessor}.each do |child_node| + new_child_and_node = #{child_class.visit_method}_with_modifications(child_node, new_node) + # Reassign `node` in case the child hook makes a modification + if new_child_and_node.is_a?(Array) + new_node = new_child_and_node[1] + end + end" + end.join("\n") + else + "" + end + } + end + end_visit(new_node, parent) unless no_hooks + if new_node.equal?(node) + [node, parent] + else + [new_node, parent] + end + end + end + def #{node_method}_with_modifications(node, parent) + new_node_and_new_parent = #{node_method}(node, parent) + apply_modifications(node, parent, new_node_and_new_parent) + end + RUBY + end + + + [ Language::Nodes::Argument, Language::Nodes::Directive, @@ -277,6 +278,8 @@ def on_argument_children(new_node) make_visit_methods(ast_node_class) end + # rubocop:enable Development/NoEvalCop + private def apply_modifications(node, parent, new_node_and_new_parent)
lib/graphql/schema/argument.rb+3 −5 modified@@ -53,6 +53,7 @@ def from_resolver? 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) arg_name ||= name @name = -(camelize ? Member::BuildType.camelize(arg_name.to_s) : arg_name.to_s) + NameValidator.validate!(@name) @type_expr = type_expr || type @description = desc || description @null = required != true @@ -88,11 +89,8 @@ def initialize(arg_name = nil, type_expr = nil, desc = nil, required: true, type end if definition_block - if definition_block.arity == 1 - instance_exec(self, &definition_block) - else - instance_eval(&definition_block) - end + # `self` will still be self, it will also be the first argument to the block: + instance_exec(self, &definition_block) end end
lib/graphql/schema/build_from_definition.rb+8 −7 modified@@ -453,17 +453,18 @@ def build_fields(owner, field_definitions, type_resolver, default_resolve:) # Don't do this for interfaces if default_resolve - owner.class_eval <<-RUBY, __FILE__, __LINE__ - # frozen_string_literal: true - def #{resolve_method_name}(**args) - field_instance = self.class.get_field("#{field_definition.name}") - context.schema.definition_default_resolve.call(self.class, field_instance, object, args, context) - end - RUBY + define_field_resolve_method(owner, resolve_method_name, field_definition.name) end end end + def define_field_resolve_method(owner, method_name, field_name) + owner.define_method(method_name) { |**args| + field_instance = self.class.get_field(field_name) + context.schema.definition_default_resolve.call(self.class, field_instance, object, args, context) + } + end + def build_resolve_type(lookup_hash, directives, missing_type_handler) resolve_type_proc = nil resolve_type_proc = ->(ast_node) {
lib/graphql/schema/directive.rb+1 −1 modified@@ -99,7 +99,7 @@ def repeatable(new_value) def inherited(subclass) super - subclass.class_eval do + subclass.class_exec do @default_graphql_name ||= nil end end
lib/graphql/schema/enum_value.rb+1 −1 modified@@ -47,7 +47,7 @@ def initialize(graphql_name, desc = nil, owner:, ast_node: nil, directives: nil, end if block_given? - instance_eval(&block) + instance_exec(self, &block) end end
lib/graphql/schema/field.rb+1 −1 modified@@ -233,7 +233,7 @@ def initialize(type: nil, name: nil, owner: nil, null: nil, description: NOT_CON @underscored_name = -Member::BuildType.underscore(name_s) @name = -(camelize ? Member::BuildType.camelize(name_s) : name_s) - + NameValidator.validate!(@name) @description = description @type = @owner_type = @own_validators = @own_directives = @own_arguments = @arguments_statically_coercible = nil # these will be prepared later if necessary
lib/graphql/schema/input_object.rb+8 −6 modified@@ -131,12 +131,7 @@ def argument(*args, **kwargs, &block) end end # Add a method access - method_name = argument_defn.keyword - class_eval <<-RUBY, __FILE__, __LINE__ - def #{method_name} - self[#{method_name.inspect}] - end - RUBY + define_accessor_method(argument_defn.keyword) argument_defn end @@ -242,6 +237,13 @@ def coerce_result(value, ctx) result end + + private + + def define_accessor_method(method_name) + define_method(method_name) { self[method_name] } + alias_method(method_name, method_name) + end end private
lib/graphql/schema/member/has_directives.rb+1 −1 modified@@ -6,7 +6,7 @@ class Member module HasDirectives def self.extended(child_cls) super - child_cls.module_eval { self.own_directives = nil } + child_cls.module_exec { self.own_directives = nil } end def inherited(child_cls)
lib/graphql/schema/member/has_fields.rb+1 −1 modified@@ -180,7 +180,7 @@ def self.extended(child_class) def inherited(subclass) super - subclass.class_eval do + subclass.class_exec do @own_fields ||= nil @field_class ||= nil end
lib/graphql/schema/member/has_interfaces.rb+1 −1 modified@@ -119,7 +119,7 @@ def self.extended(child_class) def inherited(subclass) super - subclass.class_eval do + subclass.class_exec do @own_interface_type_memberships ||= nil end end
lib/graphql/schema/member/type_system_helpers.rb+1 −1 modified@@ -43,7 +43,7 @@ def kind private def inherited(subclass) - subclass.class_eval do + subclass.class_exec do @to_non_null_type ||= nil @to_list_type ||= nil end
lib/graphql/tracing/appoptics_trace.rb+4 −0 modified@@ -28,6 +28,8 @@ def self.version Gem::Version.new('1.0.0') end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + [ 'lex', 'parse', @@ -55,6 +57,8 @@ def #{trace_method}(**data) RUBY end + # rubocop:enable Development/NoEvalCop + def execute_field(query:, field:, ast_node:, arguments:, object:) return_type = field.type.unwrap trace_field = if return_type.kind.scalar? || return_type.kind.enum?
lib/graphql/tracing/appsignal_trace.rb+4 −0 modified@@ -13,6 +13,8 @@ def initialize(set_action_name: false, **rest) super end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { "lex" => "lex.graphql", "parse" => "parse.graphql", @@ -43,6 +45,8 @@ def #{trace_method}(**data) RUBY end + # rubocop:enable Development/NoEvalCop + def platform_execute_field(platform_key) Appsignal.instrument(platform_key) do yield
lib/graphql/tracing/data_dog_trace.rb+4 −0 modified@@ -20,6 +20,8 @@ def initialize(tracer: nil, analytics_enabled: false, analytics_sample_rate: 1.0 super end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { 'lex' => 'lex.graphql', 'parse' => 'parse.graphql', @@ -69,6 +71,8 @@ def #{trace_method}(**data) RUBY end + # rubocop:enable Development/NoEvalCop + def execute_field_span(span_key, query, field, ast_node, arguments, object) return_type = field.type.unwrap trace_field = if return_type.kind.scalar? || return_type.kind.enum?
lib/graphql/tracing/notifications_trace.rb+4 −0 modified@@ -16,6 +16,8 @@ def initialize(engine:, **rest) super end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { "lex" => "lex.graphql", "parse" => "parse.graphql", @@ -39,6 +41,8 @@ def #{trace_method}(**metadata, &blk) RUBY end + # rubocop:enable Development/NoEvalCop + include PlatformTrace end end
lib/graphql/tracing/platform_trace.rb+5 −0 modified@@ -39,6 +39,9 @@ def self.included(child_class) include(BaseKeyCache) } child_class.const_set(:KeyCache, key_methods_class) + + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + [:execute_field, :execute_field_lazy].each do |field_trace_method| if !child_class.method_defined?(field_trace_method) child_class.module_eval <<-RUBY, __FILE__, __LINE__ @@ -91,6 +94,8 @@ def #{rt_trace_method}(query:, type:, object:) end RUBY end + + # rubocop:enable Development/NoEvalCop end end
lib/graphql/tracing/prometheus_trace.rb+4 −0 modified@@ -13,6 +13,8 @@ def initialize(client: PrometheusExporter::Client.default, keys_whitelist: ["exe super(**rest) end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { 'lex' => "graphql.lex", 'parse' => "graphql.parse", @@ -30,6 +32,8 @@ def #{trace_method}(**data, &block) RUBY end + # rubocop:enable Development/NoEvalCop + def platform_execute_field(platform_key, &block) instrument_execution(platform_key, "execute_field", &block) end
lib/graphql/tracing/scout_trace.rb+3 −0 modified@@ -16,6 +16,8 @@ def initialize(set_transaction_name: false, **_rest) super end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { "lex" => "lex.graphql", "parse" => "parse.graphql", @@ -45,6 +47,7 @@ def #{trace_method}(**data) end RUBY end + # rubocop:enable Development/NoEvalCop def platform_execute_field(platform_key, &block) self.class.instrument("GraphQL", platform_key, INSTRUMENT_OPTS, &block)
lib/graphql/tracing/statsd_trace.rb+4 −0 modified@@ -11,6 +11,8 @@ def initialize(statsd:, **rest) super(**rest) end + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + { 'lex' => "graphql.lex", 'parse' => "graphql.parse", @@ -30,6 +32,8 @@ def #{trace_method}(**data) RUBY end + # rubocop:enable Development/NoEvalCop + def platform_execute_field(platform_key, &block) @statsd.time(platform_key, &block) end
lib/graphql/types/relay/connection_behaviors.rb+1 −1 modified@@ -13,7 +13,7 @@ def self.included(child_class) child_class.node_nullable(true) child_class.edges_nullable(true) child_class.edge_nullable(true) - child_class.module_eval { + child_class.module_exec { self.edge_type = nil self.node_type = nil self.edge_class = nil
lib/graphql/types/relay/edge_behaviors.rb+1 −1 modified@@ -8,7 +8,7 @@ def self.included(child_class) child_class.description("An edge in a connection.") child_class.field(:cursor, String, null: false, description: "A cursor for use in pagination.") child_class.extend(ClassMethods) - child_class.class_eval { self.node_type = nil } + child_class.class_exec { self.node_type = nil } child_class.node_nullable(true) end
lib/graphql/version.rb+1 −1 modified@@ -1,4 +1,4 @@ # frozen_string_literal: true module GraphQL - VERSION = "2.0.31" + VERSION = "2.0.32" end
.rubocop.yml+5 −0 modified@@ -1,5 +1,6 @@ require: - ./cop/development/none_without_block_cop + - ./cop/development/no_eval_cop - ./cop/development/no_focus_cop - ./lib/graphql/rubocop/graphql/default_null_true - ./lib/graphql/rubocop/graphql/default_required_true @@ -52,6 +53,10 @@ Development/NoneWithoutBlockCop: - "lib/**/*" - "spec/**/*" +Development/NoEvalCop: + Include: + - "lib/**/*" + Development/NoFocusCop: Include: - "spec/**/*"
spec/graphql/schema/loader_spec.rb+149 −0 modified@@ -398,4 +398,153 @@ def assert_deep_equal(expected_type, actual_type) assert_equal arg.default_value, { 'id' => nil, 'int' => nil, 'float' => nil, 'enum' => nil, 'sub' => nil, 'bool' => nil, 'bigint' => nil } end end + + it "validates field argument names" do + json = { + "data" => { + "__schema" => { + "queryType" => { + "name" => "Query" + }, + "mutationType" => nil, + "subscriptionType" => nil, + "types" => [ + { + "kind" => "OBJECT", + "name" => "Query", + "description" => nil, + "fields" => [ + { + "name" => "int", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil, + }, + "args" => [ + { + "name" => "something-wrong", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil + }, + "defaultValue" => nil + } + ], + } + ] + } + ] + } + } + } + err = assert_raises GraphQL::InvalidNameError do + GraphQL::Schema.from_introspection(json) + end + + assert_includes err.message, "something-wrong" + end + + it "validates field names" do + json = { + "data" => { + "__schema" => { + "queryType" => { + "name" => "Query" + }, + "mutationType" => nil, + "subscriptionType" => nil, + "types" => [ + { + "kind" => "OBJECT", + "name" => "Query", + "description" => nil, + "fields" => [ + { + "name" => "bad.int", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil, + }, + "args" => [], + } + ] + } + ] + } + } + } + err = assert_raises GraphQL::InvalidNameError do + GraphQL::Schema.from_introspection(json) + end + + assert_includes err.message, "bad.int" + end + + it "validates input object argument names" do + json = { + "data" => { + "__schema" => { + "queryType" => { + "name" => "Query" + }, + "mutationType" => nil, + "subscriptionType" => nil, + "types" => [ + { + "kind" => "OBJECT", + "name" => "Query", + "description" => nil, + "fields" => [ + { + "name" => "int", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil, + }, + "args" => [ + { + "name" => "inputObject", + "description" => nil, + "type" => { + "kind" => "INPUT_OBJECT", + "name" => "SomeInputObject", + "ofType" => nil + }, + "defaultValue" => nil + } + ], + } + ] + }, + { + "kind" => "INPUT_OBJECT", + "name" => "SomeInputObject", + "description" => nil, + "inputFields" => [ + { + "name"=>"bad, input", + "type"=> { "kind" => "SCALAR", "name" => "String"}, + "defaultValue"=> nil, + "description" => nil, + }, + ] + } + ] + } + } + } + err = assert_raises GraphQL::InvalidNameError do + GraphQL::Schema.from_introspection(json) + end + + assert_includes err.message, "bad, input" + end end
6eca16b9fa55Merge commit from fork
11 files changed · +198 −21
benchmark/run.rb+1 −1 modified@@ -82,7 +82,7 @@ def self.profile_large_introspection graphql_name("Object#{n}") 20.times do |n2| field :"field#{n2}", String do - argument :arg, String + argument :input, input_obj_t end end end
cop/development/no_eval_cop.rb+18 −0 added@@ -0,0 +1,18 @@ +# frozen_string_literal: true +require 'rubocop' + +module Cop + module Development + class NoEvalCop < RuboCop::Cop::Base + 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." + + def on_send(node) + case node.method_name + when :module_eval, :class_eval, :instance_eval + message = MSG_TEMPLATE % { eval_method_name: node.method_name, exec_method_name: node.method_name.to_s.sub("eval", "exec").to_sym } + add_offense node, message: message + end + end + end + end +end
lib/graphql/language/nodes.rb+3 −0 modified@@ -133,6 +133,8 @@ def merge!(new_options) end class << self + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + # Add a default `#visit_method` and `#children_method_name` using the class name def inherited(child_class) super @@ -276,6 +278,7 @@ def initialize_node #{arguments.join(", ")} RUBY end end + # rubocop:enable Development/NoEvalCop end end
lib/graphql/schema/argument.rb+3 −5 modified@@ -52,6 +52,7 @@ def from_resolver? 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: NO_DEFAULT, as: nil, from_resolver: false, camelize: true, prepare: nil, method_access: true, owner:, validates: nil, directives: nil, deprecation_reason: nil, replace_null_with_default: false, &definition_block) arg_name ||= name @name = -(camelize ? Member::BuildType.camelize(arg_name.to_s) : arg_name.to_s) + NameValidator.validate!(@name) @type_expr = type_expr || type @description = desc || description @null = required != true @@ -85,11 +86,8 @@ def initialize(arg_name = nil, type_expr = nil, desc = nil, required: true, type end if definition_block - if definition_block.arity == 1 - instance_exec(self, &definition_block) - else - instance_eval(&definition_block) - end + # `self` will still be self, it will also be the first argument to the block: + instance_exec(self, &definition_block) end end
lib/graphql/schema/build_from_definition.rb+8 −7 modified@@ -427,17 +427,18 @@ def build_fields(owner, field_definitions, type_resolver, default_resolve:) # Don't do this for interfaces if default_resolve - owner.class_eval <<-RUBY, __FILE__, __LINE__ - # frozen_string_literal: true - def #{resolve_method_name}(**args) - field_instance = self.class.get_field("#{field_definition.name}") - context.schema.definition_default_resolve.call(self.class, field_instance, object, args, context) - end - RUBY + define_field_resolve_method(owner, resolve_method_name, field_definition.name) end end end + def define_field_resolve_method(owner, method_name, field_name) + owner.define_method(method_name) { |**args| + field_instance = self.class.get_field(field_name) + context.schema.definition_default_resolve.call(self.class, field_instance, object, args, context) + } + end + def build_resolve_type(lookup_hash, directives, missing_type_handler) resolve_type_proc = nil resolve_type_proc = ->(ast_node) {
lib/graphql/schema/enum_value.rb+1 −1 modified@@ -55,7 +55,7 @@ def initialize(graphql_name, desc = nil, owner:, ast_node: nil, directives: nil, end if block_given? - instance_eval(&block) + instance_exec(self, &block) end end
lib/graphql/schema/field.rb+1 −0 modified@@ -232,6 +232,7 @@ def initialize(type: nil, name: nil, owner: nil, null: true, field: nil, functio @underscored_name = -Member::BuildType.underscore(name_s) @name = -(camelize ? Member::BuildType.camelize(name_s) : name_s) + NameValidator.validate!(@name) @description = description if field.is_a?(GraphQL::Schema::Field) raise ArgumentError, "Instead of passing a field as `field:`, use `add_field(field)` to add an already-defined field."
lib/graphql/schema/input_object.rb+8 −6 modified@@ -138,12 +138,7 @@ class << self def argument(*args, **kwargs, &block) argument_defn = super(*args, **kwargs, &block) # Add a method access - method_name = argument_defn.keyword - class_eval <<-RUBY, __FILE__, __LINE__ - def #{method_name} - self[#{method_name.inspect}] - end - RUBY + define_accessor_method(argument_defn.keyword) argument_defn end @@ -253,6 +248,13 @@ def coerce_result(value, ctx) result end + + private + + def define_accessor_method(method_name) + define_method(method_name) { self[method_name] } + alias_method(method_name, method_name) + end end private
lib/graphql/version.rb+1 −1 modified@@ -1,4 +1,4 @@ # frozen_string_literal: true module GraphQL - VERSION = "1.13.23" + VERSION = "1.13.24" end
.rubocop.yml+5 −0 modified@@ -1,5 +1,6 @@ require: - ./cop/development/none_without_block_cop + - ./cop/development/no_eval_cop - ./cop/development/no_focus_cop - ./lib/graphql/rubocop/graphql/default_null_true - ./lib/graphql/rubocop/graphql/default_required_true @@ -52,6 +53,10 @@ Development/NoneWithoutBlockCop: - "lib/**/*" - "spec/**/*" +Development/NoEvalCop: + Include: + - "lib/**/*" + Development/NoFocusCop: Include: - "spec/**/*"
spec/graphql/schema/loader_spec.rb+149 −0 modified@@ -398,4 +398,153 @@ def assert_deep_equal(expected_type, actual_type) assert_equal arg.default_value, { 'id' => nil, 'int' => nil, 'float' => nil, 'enum' => nil, 'sub' => nil, 'bool' => nil, 'bigint' => nil } end end + + it "validates field argument names" do + json = { + "data" => { + "__schema" => { + "queryType" => { + "name" => "Query" + }, + "mutationType" => nil, + "subscriptionType" => nil, + "types" => [ + { + "kind" => "OBJECT", + "name" => "Query", + "description" => nil, + "fields" => [ + { + "name" => "int", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil, + }, + "args" => [ + { + "name" => "something-wrong", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil + }, + "defaultValue" => nil + } + ], + } + ] + } + ] + } + } + } + err = assert_raises GraphQL::InvalidNameError do + GraphQL::Schema.from_introspection(json) + end + + assert_includes err.message, "something-wrong" + end + + it "validates field names" do + json = { + "data" => { + "__schema" => { + "queryType" => { + "name" => "Query" + }, + "mutationType" => nil, + "subscriptionType" => nil, + "types" => [ + { + "kind" => "OBJECT", + "name" => "Query", + "description" => nil, + "fields" => [ + { + "name" => "bad.int", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil, + }, + "args" => [], + } + ] + } + ] + } + } + } + err = assert_raises GraphQL::InvalidNameError do + GraphQL::Schema.from_introspection(json) + end + + assert_includes err.message, "bad.int" + end + + it "validates input object argument names" do + json = { + "data" => { + "__schema" => { + "queryType" => { + "name" => "Query" + }, + "mutationType" => nil, + "subscriptionType" => nil, + "types" => [ + { + "kind" => "OBJECT", + "name" => "Query", + "description" => nil, + "fields" => [ + { + "name" => "int", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil, + }, + "args" => [ + { + "name" => "inputObject", + "description" => nil, + "type" => { + "kind" => "INPUT_OBJECT", + "name" => "SomeInputObject", + "ofType" => nil + }, + "defaultValue" => nil + } + ], + } + ] + }, + { + "kind" => "INPUT_OBJECT", + "name" => "SomeInputObject", + "description" => nil, + "inputFields" => [ + { + "name"=>"bad, input", + "type"=> { "kind" => "SCALAR", "name" => "String"}, + "defaultValue"=> nil, + "description" => nil, + }, + ] + } + ] + } + } + } + err = assert_raises GraphQL::InvalidNameError do + GraphQL::Schema.from_introspection(json) + end + + assert_includes err.message, "bad, input" + end end
28233b16c0ebMerge commit from fork
9 files changed · +193 −20
cop/development/no_eval_cop.rb+18 −0 added@@ -0,0 +1,18 @@ +# frozen_string_literal: true +require 'rubocop' + +module Cop + module Development + class NoEvalCop < RuboCop::Cop::Base + 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." + + def on_send(node) + case node.method_name + when :module_eval, :class_eval, :instance_eval + message = MSG_TEMPLATE % { eval_method_name: node.method_name, exec_method_name: node.method_name.to_s.sub("eval", "exec").to_sym } + add_offense node, message: message + end + end + end + end +end
lib/graphql/language/nodes.rb+3 −0 modified@@ -133,6 +133,8 @@ def merge!(new_options) end class << self + # rubocop:disable Development/NoEvalCop This eval takes static inputs at load-time + # Add a default `#visit_method` and `#children_method_name` using the class name def inherited(child_class) super @@ -276,6 +278,7 @@ def initialize_node #{arguments.join(", ")} RUBY end end + # rubocop:enable Development/NoEvalCop end end
lib/graphql/schema/argument.rb+3 −5 modified@@ -55,6 +55,7 @@ def from_resolver? def initialize(arg_name = nil, type_expr = nil, desc = nil, required:, type: nil, name: nil, loads: nil, description: nil, ast_node: nil, default_value: NO_DEFAULT, as: nil, from_resolver: false, camelize: true, prepare: nil, method_access: true, owner:, validates: nil, directives: nil, deprecation_reason: nil, &definition_block) arg_name ||= name @name = -(camelize ? Member::BuildType.camelize(arg_name.to_s) : arg_name.to_s) + NameValidator.validate!(@name) @type_expr = type_expr || type @description = desc || description @null = !required @@ -78,11 +79,8 @@ def initialize(arg_name = nil, type_expr = nil, desc = nil, required:, type: nil self.validates(validates) if definition_block - if definition_block.arity == 1 - instance_exec(self, &definition_block) - else - instance_eval(&definition_block) - end + # `self` will still be self, it will also be the first argument to the block: + instance_exec(self, &definition_block) end end
lib/graphql/schema/build_from_definition.rb+8 −7 modified@@ -426,17 +426,18 @@ def build_fields(owner, field_definitions, type_resolver, default_resolve:) # Don't do this for interfaces if default_resolve - owner.class_eval <<-RUBY, __FILE__, __LINE__ - # frozen_string_literal: true - def #{resolve_method_name}(**args) - field_instance = self.class.get_field("#{field_definition.name}") - context.schema.definition_default_resolve.call(self.class, field_instance, object, args, context) - end - RUBY + define_field_resolve_method(owner, resolve_method_name, field_definition.name) end end end + def define_field_resolve_method(owner, method_name, field_name) + owner.define_method(method_name) { |**args| + field_instance = self.class.get_field(field_name) + context.schema.definition_default_resolve.call(self.class, field_instance, object, args, context) + } + end + def build_resolve_type(lookup_hash, directives, missing_type_handler) resolve_type_proc = nil resolve_type_proc = ->(ast_node) {
lib/graphql/schema/enum_value.rb+1 −1 modified@@ -55,7 +55,7 @@ def initialize(graphql_name, desc = nil, owner:, ast_node: nil, directives: nil, end if block_given? - instance_eval(&block) + instance_exec(self, &block) end end
lib/graphql/schema/field.rb+1 −0 modified@@ -231,6 +231,7 @@ def initialize(type: nil, name: nil, owner: nil, null: nil, field: nil, function name_s = -name.to_s @underscored_name = -Member::BuildType.underscore(name_s) @name = -(camelize ? Member::BuildType.camelize(name_s) : name_s) + NameValidator.validate!(@name) @description = description if field.is_a?(GraphQL::Schema::Field) raise ArgumentError, "Instead of passing a field as `field:`, use `add_field(field)` to add an already-defined field."
lib/graphql/schema/input_object.rb+9 −6 modified@@ -123,12 +123,8 @@ class << self def argument(*args, **kwargs, &block) argument_defn = super(*args, **kwargs, &block) # Add a method access - method_name = argument_defn.keyword - class_eval <<-RUBY, __FILE__, __LINE__ - def #{method_name} - self[#{method_name.inspect}] - end - RUBY + define_accessor_method(argument_defn.keyword) + argument_defn end def to_graphql @@ -236,6 +232,13 @@ def coerce_result(value, ctx) result end + + private + + def define_accessor_method(method_name) + define_method(method_name) { self[method_name] } + alias_method(method_name, method_name) + end end private
lib/graphql/version.rb+1 −1 modified@@ -1,4 +1,4 @@ # frozen_string_literal: true module GraphQL - VERSION = "1.12.24" + VERSION = "1.12.25" end
spec/graphql/schema/loader_spec.rb+149 −0 modified@@ -389,4 +389,153 @@ def assert_deep_equal(expected_type, actual_type) assert_equal arg.default_value, { 'id' => nil, 'int' => nil, 'float' => nil, 'enum' => nil, 'sub' => nil, 'bool' => nil, 'bigint' => nil } end end + + it "validates field argument names" do + json = { + "data" => { + "__schema" => { + "queryType" => { + "name" => "Query" + }, + "mutationType" => nil, + "subscriptionType" => nil, + "types" => [ + { + "kind" => "OBJECT", + "name" => "Query", + "description" => nil, + "fields" => [ + { + "name" => "int", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil, + }, + "args" => [ + { + "name" => "something-wrong", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil + }, + "defaultValue" => nil + } + ], + } + ] + } + ] + } + } + } + err = assert_raises GraphQL::InvalidNameError do + GraphQL::Schema.from_introspection(json) + end + + assert_includes err.message, "something-wrong" + end + + it "validates field names" do + json = { + "data" => { + "__schema" => { + "queryType" => { + "name" => "Query" + }, + "mutationType" => nil, + "subscriptionType" => nil, + "types" => [ + { + "kind" => "OBJECT", + "name" => "Query", + "description" => nil, + "fields" => [ + { + "name" => "bad.int", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil, + }, + "args" => [], + } + ] + } + ] + } + } + } + err = assert_raises GraphQL::InvalidNameError do + GraphQL::Schema.from_introspection(json) + end + + assert_includes err.message, "bad.int" + end + + it "validates input object argument names" do + json = { + "data" => { + "__schema" => { + "queryType" => { + "name" => "Query" + }, + "mutationType" => nil, + "subscriptionType" => nil, + "types" => [ + { + "kind" => "OBJECT", + "name" => "Query", + "description" => nil, + "fields" => [ + { + "name" => "int", + "description" => nil, + "type" => { + "kind" => "SCALAR", + "name" => "Int", + "ofType" => nil, + }, + "args" => [ + { + "name" => "inputObject", + "description" => nil, + "type" => { + "kind" => "INPUT_OBJECT", + "name" => "SomeInputObject", + "ofType" => nil + }, + "defaultValue" => nil + } + ], + } + ] + }, + { + "kind" => "INPUT_OBJECT", + "name" => "SomeInputObject", + "description" => nil, + "inputFields" => [ + { + "name"=>"bad, input", + "type"=> { "kind" => "SCALAR", "name" => "String"}, + "defaultValue"=> nil, + "description" => nil, + }, + ] + } + ] + } + } + } + err = assert_raises GraphQL::InvalidNameError do + GraphQL::Schema.from_introspection(json) + end + + assert_includes err.message, "bad, input" + end end
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
14- github.com/advisories/GHSA-q92j-grw3-h492ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2025-27407ghsaADVISORY
- about.gitlab.com/releases/2025/03/12/patch-release-gitlab-17-9-2-releasednvdWEB
- github.com/rmosolgo/graphql-ruby/commit/28233b16c0eb9d0fb7808f4980e061dc7507c4cdnvdWEB
- github.com/rmosolgo/graphql-ruby/commit/2d2f4ed1f79472f8eed29c864b039649e1de238fnvdWEB
- github.com/rmosolgo/graphql-ruby/commit/5c5a7b9a9bdce143be048074aea50edb7bb747benvdWEB
- github.com/rmosolgo/graphql-ruby/commit/6eca16b9fa553aa957099a30dbde64ddcdac52canvdWEB
- github.com/rmosolgo/graphql-ruby/commit/d0963289e0dab4ea893bbecf12bb7d89294957bbnvdWEB
- github.com/rmosolgo/graphql-ruby/commit/d1117ae0361d9ed67e0795b07f5c3e98e62f3c7cnvdWEB
- github.com/rmosolgo/graphql-ruby/commit/e3b33ace05391da2871c75ab4d3b66e29133b367nvdWEB
- github.com/rmosolgo/graphql-ruby/commit/e58676c70aa695e3052ba1fbc787efee4ba7d67eghsaWEB
- github.com/rmosolgo/graphql-ruby/security/advisories/GHSA-q92j-grw3-h492nvdWEB
- github.com/rubysec/ruby-advisory-db/blob/master/gems/graphql/CVE-2025-27407.ymlghsaWEB
- lists.debian.org/debian-lts-announce/2025/08/msg00002.htmlnvdWEB
News mentions
0No linked articles in our index yet.