GHSA-MPWP-4H2M-765C

Active Job's Identity Crisis: Object Injection in Rails 4.2

Amit Schendel
Amit Schendel
Senior Security Researcher

Jan 17, 2026·6 min read

Executive Summary (TL;DR)

Active Job tried to be too smart for its own good. In early Rails 4.2 betas, it automatically converted any string starting with `gid://` into a Ruby object. Attackers could exploit this to force the application to load and execute the `.find()` method on arbitrary classes, leading to potential authorization bypasses or worse, depending on the available gadgets.

A critical object injection vulnerability in Ruby on Rails' Active Job component (versions < 4.2.0.beta2) allows attackers to instantiate arbitrary application objects by passing specially crafted strings starting with the 'gid://' protocol.

The Hook: Magic is Dangerous

Rails is famous for its 'Convention over Configuration' philosophy, which is often just a polite way of saying 'Magic we hide from you until it bites your hand off.' When Rails 4.2 introduced Active Job, it brought a shiny new feature: the ability to seamlessly pass database objects to background workers.

Here's the pitch: You enqueue a job with User.find(1), and when the job runs in Sidekiq or Resque, that argument magically wakes up as a fully instantiated User object. No more manually passing IDs and looking them up yourself. It was elegant. It was convenient.

Under the hood, this magic relied on Global IDs—URIs that look like gid://app/User/1. The system serializes the object to this string, and the worker deserializes it back. The problem? The deserializer didn't just look for serialized Global IDs; it trusted anything that looked like one. It's the software equivalent of letting anyone into the VIP section just because they're wearing a generic black t-shirt.

The Flaw: String Theory

The vulnerability lies in the simplicity of the check. In the affected versions (< 4.2.0.beta2), the Active Job deserializer iterated through job arguments and applied a very naive heuristic: 'Is this a string? Does it start with gid://? If yes, it's an object.'

This is a classic Type Confusion issue. The application failed to distinguish between a string intended to be a Global ID and a string provided by a user that happens to look like one. If your application allowed a user to input a string that eventually got passed to a background job—say, a log message, a comment body, or a webhook payload—that user could supply a malicious URI.

When the worker processes the job, it sees the string gid://MyApp/DangerousClass/666, assumes it's a valid reference, and passes it to GlobalID::Locator.locate(). This method parses the URI, constantizes the class name (DangerousClass), and blindly calls .find('666') on it. The attacker is no longer passing data; they are invoking code.

The Code: The Smoking Gun

Let's look at the logic that caused the headache. The deserialization code was essentially scanning for the protocol prefix. Here is a simplified representation of the vulnerable logic:

# Vulnerable Deserializer Logic
def deserialize_argument(argument)
  if argument.is_a?(String) && argument.start_with?("gid://")
    # Oh, look! A Global ID! Let's instantiate it!
    GlobalID::Locator.locate(argument)
  else
    argument
  end
end

The fix, introduced in 4.2.0.beta2, realized that strings are untrustworthy. The Rails team wrapped the Global ID in a specific Hash structure during serialization. This ensures that only explicitly marked objects undergo deserialization.

# Patched Deserializer Logic
def deserialize_argument(argument)
  # Now we require a specific Hash structure
  if argument.is_a?(Hash) && argument.key?("_aj_globalid")
    GlobalID::Locator.locate(argument["_aj_globalid"])
  else
    argument
  end
end

By moving from a content-based check (does the string look like X?) to a structure-based check (is the data wrapped in envelope Y?), the vulnerability was neutralized.

The Exploit: Weaponizing .find()

To exploit this, we don't need memory corruption or buffer overflows. We just need a class—any class—in the Rails application that responds to the .find(id) method. This is our 'gadget'.

The Scenario: Imagine a background job that sends a welcome email. It takes the user's name as a string argument: WelcomeJob.perform_later("Alice").

The Attack: An attacker manages to manipulate the input (perhaps via a profile name field that isn't sanitized before being queued) to be gid://app/AdminConfig/1.

When the worker picks up the job, instead of sending an email to "Alice", it attempts to load the AdminConfig object with ID 1.

  1. Authorization Bypass: If the job logic assumes arguments are simple strings, it might log the output or pass it to another system. Suddenly, the internal representation of your AdminConfig or SuperUser is floating around in variables where it shouldn't be.
  2. Side Effects: The .find method in some models triggers SQL queries, logging, or even external API calls. If a model's .find method is complex, an attacker could trigger a Denial of Service (DoS) by requesting a resource-intensive ID.
  3. Remote Code Execution (Theoretical): If there exists a class with a .find method that performs unsafe operations (like eval or system) using the ID, this becomes full RCE. While rare in standard Rails models, custom classes or gems often introduce surprising gadgets.

The Impact: Why Should We Panic?

The CVSS score is 8.7 (High) for a reason. This is a logic flaw that breaks the boundary between 'data' and 'code'. While it doesn't offer the immediate RCE gratification of a standard Marshal.load deserialization bug, it turns every model in your application into a potential attack vector.

In a complex monolithic Rails app, there are hundreds of classes. Can you guarantee that none of them have side effects in their .find method? Can you guarantee that instantiating a sensitive object in the wrong context won't leak data?

Furthermore, this attack bypasses standard authentication checks. The background worker typically runs with elevated privileges (access to all DB records). If an attacker can trick the worker into loading a record the attacker shouldn't see, and then the worker includes that object's string representation in a log file or an outbound email, data exfiltration is achieved.

The Fix: Wrapping the Poison

The mitigation is straightforward: Update to Rails 4.2.0.beta2 or later. The fix fundamentally changes the wire format of Active Job arguments.

Instead of sending: ["gid://app/User/1"]

The system now sends: [{ "_aj_globalid": "gid://app/User/1" }]

This creates a namespace for the serialization logic. Unless the attacker can control the structure of the arguments (i.e., inject a JSON object instead of a string), the gid:// string is treated as just text.

If you cannot upgrade (though you really should, 4.2.0.beta1 is ancient), you would need to monkey-patch ActiveJob::Arguments to refuse deserialization of bare strings, effectively backporting the logic shown in the 'The Code' section above.

Technical Appendix

CVSS Score
8.7/ 10
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N
EPSS Probability
0.10%

Affected Systems

Ruby on Rails (Active Job)Applications using GlobalID

Affected Versions Detail

Product
Affected Versions
Fixed Version
activejob
Rails
< 4.2.0.beta24.2.0.beta2
AttributeDetail
CWE IDCWE-74 (Improper Neutralization of Special Elements)
Attack VectorNetwork (Job Queue)
CVSS v3.18.7 (High)
ImpactIntegrity / Object Injection
Affected ComponentActiveJob::Arguments#deserialize
Exploit StatusPoC Available (Theoretical)
CWE-74
Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')

Subscribe to updates

Get the latest CVE analysis reports delivered to your inbox.