CVEReports
CVEReports

Automated vulnerability intelligence platform. Comprehensive reports for high-severity CVEs generated by AI.

Product

  • Home
  • Sitemap
  • RSS Feed

Company

  • About
  • Contact
  • Privacy Policy
  • Terms of Service

© 2026 CVEReports. All rights reserved.

Made with love by Amit Schendel & Alon Barad



GHSA-MPWP-4H2M-765C
6.60.04%

The Ghost in the Machine: Active Job's GlobalID Identity Crisis

Alon Barad
Alon Barad
Software Engineer

Feb 17, 2026·7 min read·3 visits

PoC Available

Executive Summary (TL;DR)

Active Job versions prior to 4.2.0.beta2 treated any string starting with 'gid://' as a GlobalID reference, automatically locating and instantiating the corresponding object. Attackers could exploit this to inject arbitrary database records into background jobs.

A historical but critical flaw in Ruby on Rails' Active Job component allowed attackers to perform object injection via GlobalID strings. By crafting specific string arguments containing the 'gid://' prefix, malicious actors could force the application to instantiate arbitrary Active Record models during background job processing. This 'magic' deserialization bypasses intended access controls and logic.

The Hook: Rails Magic - It's a Feature Until It's a Bug

Ruby on Rails is famous for its "magic"—the convention-over-configuration philosophy that makes developers' lives easier but occasionally makes security researchers' lives much more interesting. One of the crown jewels of this magic is Active Job, introduced around Rails 4.2. It unified background processing, allowing devs to write jobs that run on Sidekiq, Resque, or Delayed Job without changing a line of code.

But the real magic trick was GlobalID. In the old days, if you wanted to pass a user to a background job, you passed the user_id (an integer) and fetched the record manually inside the job. Active Job said, "Nah, that's too much typing." With Active Job, you could pass the actual User object: SendWelcomeEmailJob.perform_later(user). The framework would quietly serialize this into a URI like gid://my-app/User/1 and re-materialize it into a User object when the worker picked it up.

It sounds convenient, doesn't it? The problem is that convenience in serialization often comes at the cost of strict typing. When a system tries to be too smart about converting data back into objects, it often forgets to ask: "Wait, did the user actually intend for this string to become a database query?"

The Flaw: If It Quacks Like a GlobalID...

The vulnerability, now tracked as GHSA-MPWP-4H2M-765C, lies in how Active Job decided what to deserialize. In the affected beta versions, the logic was dangerously simple. It operated on a principle roughly equivalent to: "If it looks like a GlobalID, it must be a GlobalID."

Specifically, the argument deserializer iterated through the arguments passed to a job. If it encountered a String that started with the prefix gid://, it immediately assumed, "Aha! This is a reference to a model!" and passed it to GlobalID::Locator.locate.

This is a classic case of improper type distinction. The system failed to differentiate between a literal string containing a GID URI and a serialized reference to an object. If a developer wrote a job meant to log a URL or a message, and an attacker managed to pass the string gid://app/User/1 into that argument, Active Job wouldn't treat it as text. It would treat it as a command to fetch User #1 from the database.

This meant any input vector that eventually flowed into an ActiveJob argument—even indirectly—became a potential vector for Object Injection. You aren't getting full RCE (Remote Code Execution) immediately like you might with Python's pickle or Java's ObjectInputStream, but you are forcing the application to instantiate objects it never intended to touch in that context.

The Code: String Theory vs. Reality

Let's look at the logic that caused this headache. The vulnerable deserialization code effectively looked something like this (simplified for clarity):

# Vulnerable Logic (Conceptual)
def deserialize_argument(argument)
  if argument.is_a?(String) && argument.start_with?("gid://")
    GlobalID::Locator.locate(argument)
  else
    argument
  end
end

Do you see the issue? There is no metadata wrapping the value to say "This was an object." It just trusts the string content. If I type gid://... into a form field that gets passed to a background job, I trigger a database lookup.

The fix, implemented in version 4.2.0.beta2, introduced a structured wrapper. Instead of relying on the string value itself, Rails decided to wrap complex objects in a special hash structure during serialization. The new format looks for a specific key, often _aj_globalid.

# Secure Logic (Post-Fix)
def deserialize_argument(argument)
  if argument.is_a?(Hash) && argument.key?("_aj_globalid")
    GlobalID::Locator.locate(argument["_aj_globalid"])
  else
    argument
  end
end

Now, if an attacker sends the string gid://app/User/1, the deserializer sees it as just a String. It doesn't match the Hash structure required for object hydration. The magic is now contained within a specific, internal protocol rather than being applied to every string in the universe.

The Exploit: Forging Identity Cards

To exploit this, we need a scenario where user input controls an argument passed to perform_later. This is more common than you'd think in dynamic applications or those using generic "Audit" or "Notification" jobs.

Scenario: Imagine an application has a LogEventJob that takes a message string and logs it to an external service.

# Controller
def create
  # User input directly passed to job
  LogEventJob.perform_later(params[:message])
  render plain: "Logged!"
end

The Attack:

  1. Recon: The attacker knows the app uses Rails < 4.2.0.beta2 and likely has a User model.
  2. Payload Construction: The attacker constructs a GlobalID for an administrative user. gid://my-app/User/1 (assuming ID 1 is admin).
  3. Injection: The attacker sends a POST request with message=gid://my-app/User/1.
  4. Execution:
    • The request hits the controller.
    • LogEventJob is enqueued into Redis/Sidekiq. The payload in Redis is just the string.
    • The Worker picks up the job.
    • Vulnerability Trigger: Active Job sees gid://... and calls GlobalID::Locator.locate.
    • The User model with ID 1 is instantiated from the database.

Now, instead of the job receiving the string "gid://...", it receives the actual User object.

If LogEventJob performs any method calls on the argument, like message.to_s or message.length, it might be fine. But if the job logic does something like:

def perform(event)
  # If event is a User object, this might expose sensitive data
  logger.info "Processing event: #{event.attributes}"
end

The logs now contain the admin's password hash, email, and API keys. Or worse, if the job calls a method that exists on both the expected string and the injected object but has side effects on the object (like delete or update), you have a serious logic bomb.

The Impact: Instantiation != RCE (Usually)

It is important to temper the panic here. This is Object Injection, not arbitrary code execution (usually). When the vulnerability triggers, it hydrates a record from the database. It does not necessarily execute code defined in that class unless the job interacts with it.

However, the risks are still significant:

  1. Information Disclosure: As seen in the exploit scenario, if the job logs or outputs the argument, you can dump database records you shouldn't have access to.
  2. Logic Bugs: If the job has conditional logic like if arg.is_a?(User), you can force code paths that were intended only for authenticated system processes.
  3. Side Effects: Some models have after_find or after_initialize hooks. Just loading the record might trigger audit logs, state changes, or cache invalidations that disrupt the system.
  4. DoS: You could potentially reference massive objects or trigger complex SQL joins if the GlobalID lookup logic is complex.

It is a "silent killer" vulnerability. You might not see a shell pop, but your data is leaking, and your logic is bending in ways the original developer never anticipated.

The Fix: Explicit is Better Than Implicit

The remediation is straightforward: Update Rails. Specifically, ensure your activejob gem is version 4.2.0.beta2 or higher. Given that this is a historical vulnerability from the Rails 4.x era, if you are reading this and still running a vulnerable version, you have much bigger problems than just this CVE.

For researchers and developers, the lesson is clear: Never trust data types based solely on string content. Serialization formats should always include an explicit envelope (like a specific JSON key or a binary header) that differentiates between "this is a string of text" and "this is a serialized object."

If you are stuck maintaining a legacy system (condolences), you can mitigate this by sanitizing inputs before they reach perform_later, ensuring that no user-controlled string starts with gid://. But really, just update the gem. It's been a decade.

Official Patches

Ruby on RailsComparison view showing changes between beta versions.

Technical Appendix

CVSS Score
6.6/ 10
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N/E:U
EPSS Probability
0.04%
Top 100% most exploited

Affected Systems

Ruby on Rails (Active Job component)Applications using GlobalID

Affected Versions Detail

Product
Affected Versions
Fixed Version
Active Job
Ruby on Rails
< 4.2.0.beta24.2.0.beta2
AttributeDetail
CWECWE-502 (Deserialization of Untrusted Data)
CVSS v4.06.6 (Medium)
Attack VectorNetwork
ImpactHigh Integrity / Object Injection
StatusPatched (Historical)
Affected ComponentActive Job / GlobalID

MITRE ATT&CK Mapping

T1190Exploit Public-Facing Application
Initial Access
T1059.006Command and Scripting Interpreter: Python
Execution
CWE-502
Deserialization of Untrusted Data

The application deserializes untrusted data without sufficiently verifying that the resulting data will be valid.

Known Exploits & Detection

HypotheticalConstructing a GlobalID string to force model instantiation in background jobs.

Vulnerability Timeline

Rails 4.2.0.beta1 released (Vulnerable)
2014-08-20
Rails 4.2.0.beta2 released (Patched)
2014-10-30
GHSA Advisory Published (Backfilled)
2026-01-16

References & Sources

  • [1]GitHub Advisory GHSA-MPWP-4H2M-765C
  • [2]GlobalID Documentation

Attack Flow Diagram

Press enter or space to select a node. You can then use the arrow keys to move the node around. Press delete to remove it and escape to cancel.
Press enter or space to select an edge. You can then press delete to remove it or escape to cancel.