Nov 13, 2026·11 min read·1 visit
Action Pack ignores request.session_options[:skip] when automatically upgrading rotated or hybrid-serialized cookies, causing Active Storage proxy endpoints to emit Set-Cookie headers on public cacheable responses. Shared caches storing these headers can distribute user session identifiers to unauthorized third parties.
An information exposure vulnerability in Ruby on Rails Action Pack allows sensitive session cookies to be attached to public, cacheable responses served by Active Storage proxy controllers. When cookie secret rotation or a hybrid cookie serializer migration is enabled, reading a legacy-format cookie triggers an automatic cookie rewrite that ignores the session-skipping directive. If an intermediate shared cache or CDN caches the resulting response along with its Set-Cookie header, subsequent users requesting the same URL receive the leaked session token.
Ruby on Rails includes Action Pack (actionpack), which manages web requests and responses, and Active Storage (activestorage), which facilitates uploading and serving files from cloud storage services. Active Storage supports two primary delivery mechanisms: redirect mode, which generates temporary presigned URLs, and proxy mode, which streams blob data directly through the Rails application via ActiveStorage::Blobs::ProxyController and ActiveStorage::Representations::ProxyController.
Because proxying large files through the application tier is resource-intensive, Active Storage proxy controllers return responses with aggressive HTTP caching directives (Cache-Control: max-age=3155695200, public, immutable). These directives instruct downstream Content Delivery Networks (CDNs) and reverse proxies to store the response and serve it to all subsequent clients requesting the same asset URL. To prevent user-specific credentials from polluting these shared cache entries, the controllers include the ActiveStorage::DisableSession concern, which sets request.session_options[:skip] = true.
CVE-2026-126077 (tracked also as GHSA-x6mj-8jx9-qhqv) is an information exposure vulnerability (CWE-200) resulting from an architectural flaw in how ActionDispatch::Cookies interacts with request.session_options[:skip]. When an application is configured with cookie secret rotation (config.action_dispatch.cookies_rotations) or a serializer format migration (config.action_dispatch.cookies_serializer = :hybrid), reading an incoming cookie formatted under the legacy parameters triggers an automatic write-back in the updated format. This automatic upgrade mechanism failed to check whether the session was marked to be skipped.
Consequently, when a client carrying a legacy-formatted session cookie requests a file from an Active Storage proxy controller, the controller loads the session, triggers the cookie upgrade, and attaches a Set-Cookie header containing the user's rewritten session credential to the public, immutable response. If a shared intermediate cache stores this response alongside the Set-Cookie header, the session credential is served to subsequent requesters, enabling session hijacking or session fixation. This vulnerability acts as a direct bypass of the mitigation previously introduced for CVE-2024-26144.
The root cause of CVE-2026-126077 lies in the state management of ActionDispatch::Cookies::CookieJar and its subclass ChainedCookieJars during automatic cookie reserialization. Rails provides seamless rotation for signing secrets, encryption ciphers, and serialization formats (such as migrating from Marshal to JSON). When a request arrives, the cookie middleware parses incoming signed or encrypted cookies dynamically upon access.
Within ActionDispatch::Cookies::ChainedCookieJars#parse, the method deserializes the incoming raw cookie value (dumped). It then checks whether the cookie requires reserialization via if force_reserialize || reserialize?(dumped). The reserialize? predicate returns true if the message verifier or encryptor had to fall back to a rotated secret defined in config.action_dispatch.cookies_rotations, or if the :hybrid serializer detected a legacy Marshal-encoded payload.
When reserialize?(dumped) evaluates to true, parse immediately executes self[name] = { value: value } to stage the newly formatted cookie for the outgoing HTTP response. In CookieJar#[]=, this assignment populates the internal @set_cookies hash. Prior to the patch, @set_cookies made no distinction between cookies explicitly assigned by application controller logic and cookies implicitly staged by the internal format-upgrade routine.
Although ActiveStorage::DisableSession sets request.session_options[:skip] = true to suppress session emission by the Rack session middleware, the Active Storage proxy controllers still access and load the request session during request processing. Loading the session causes ActionDispatch::Cookies to read and parse the incoming session cookie. If the incoming session cookie matches the rotation criteria, it is placed directly into @set_cookies.
At the conclusion of the request lifecycle, CookieJar#write(response) iterates over every entry in @set_cookies and invokes response.set_cookie(name, value). Because CookieJar#write did not consult request.session_options[:skip], the upgraded session cookie was unconditionally written to the outgoing response headers, overriding the session suppression guarantee required by ActiveStorage::DisableSession.
The vulnerability was resolved in commits 849a6c30eacbb6ca4722132f43c4333a93188556 (for the 8.1.x branch) and 0e6cf8faa89caf4c64311d35ca2bc88c1458d4e3 (for the 8.0.x branch) within actionpack/lib/action_dispatch/middleware/cookies.rb. The patch introduces state tracking inside CookieJar to differentiate between automatic cookie upgrades and explicit application-level cookie writes.
In the unpatched code, ChainedCookieJars#parse staged upgraded cookies using the standard setter without recording the origin of the write:
# Unpatched code in actionpack/lib/action_dispatch/middleware/cookies.rb
def parse(name, dumped, force_reserialize: false, **)
# ... deserialization logic ...
self[name] = { value: value } if force_reserialize || reserialize?(dumped)
value
endAnd during response finalization, CookieJar#write emitted all staged cookies unconditionally:
# Unpatched code in actionpack/lib/action_dispatch/middleware/cookies.rb
def write(response)
@set_cookies.each do |name, value|
if write_cookie?(value)
response.set_cookie(name, value)
end
end
# ...
endThe patch modifies CookieJar#initialize to instantiate a @upgraded_cookies = Set.new tracker. When ChainedCookieJars#parse triggers an automatic reserialization, it now invokes upgraded(name), which delegates to the parent CookieJar and adds the cookie name to @upgraded_cookies:
# Patched code in actionpack/lib/action_dispatch/middleware/cookies.rb
def initialize(request)
@set_cookies = {}
@delete_cookies = {}
@upgraded_cookies = Set.new # Tracks cookies staged exclusively by rotation/upgrade
@request = request
@cookies = {}
@committed = false
end
def []=(name, options)
# ... option handling ...
handle_options(options)
# Remove from @upgraded_cookies if set, ensuring explicit writes are preserved
@upgraded_cookies.delete(name.to_s)
# ...
end
def upgraded(name)
@upgraded_cookies << name.to_s
end
def write(response)
@set_cookies.each do |name, value|
# Suppress automatic upgrades when the response skips the session
next if @upgraded_cookies.include?(name) && request.session_options[:skip]
if write_cookie?(value)
response.set_cookie(name, value)
end
end
# ...
endWithin ChainedCookieJars, parse is updated to mark the cookie as upgraded immediately after calling self[name] = { value: value }:
# Patched code in ChainedCookieJars#parse
if force_reserialize || reserialize?(dumped)
self[name] = { value: value }
upgraded(name)
end> [!NOTE]
> Notice the order of operations: self[name] = { value: value } invokes CookieJar#[]=, which executes @upgraded_cookies.delete(name.to_s). Immediately afterward, parse calls upgraded(name) to insert name.to_s into @upgraded_cookies. If controller code later sets cookies[name] = new_value during the same request, CookieJar#[]= deletes the key from @upgraded_cookies, allowing intentional non-session cookies to be written even when request.session_options[:skip] is true.
The fix is complete for this code path. By tying the suppression of @upgraded_cookies directly to request.session_options[:skip] inside CookieJar#write, any controller or middleware that disables session output is protected from inadvertent Set-Cookie headers generated by cookie rotation or serializer migration. The legacy cookie remains unmodified on the client until a subsequent request hits an endpoint that does not skip the session.
Exploiting CVE-2026-126077 requires four architectural and operational conditions to align:
rails_storage_proxy or ActiveStorage::Blobs::ProxyController).config.action_dispatch.cookies_rotations (for signed or encrypted cookies) or config.action_dispatch.cookies_serializer = :hybrid.Cache-Control: public, immutable without stripping or bypassing responses containing Set-Cookie headers.Two distinct attack scenarios are possible under these conditions: passive session leakage (hijacking) and active session fixation.
In the passive session hijacking scenario, a legitimate authenticated user who has not visited the application since before a secret rotation requests a public image or document served via an Active Storage proxy URL. The user's browser sends their old-format session cookie. Rails parses the cookie, rotates it to the current secret, and returns the file with Cache-Control: max-age=3155695200, public, immutable and Set-Cookie: _app_session=<upgraded_cookie>. The upstream CDN caches the response and the Set-Cookie header. Any attacker or anonymous user who subsequently requests the same asset URL from the CDN receives the victim's active session cookie and assumes their authenticated identity.
In the active session fixation scenario, an attacker possessing an account (or an unauthenticated session created prior to secret rotation, or crafted if an old rotated secret was compromised) intentionally sends a request with a legacy-formatted cookie to a high-traffic, uncached Active Storage proxy asset URL. This primes the CDN cache with the attacker's upgraded session cookie. Subsequent victims who load the asset receive the attacker-controlled session cookie via Set-Cookie, fixing their browser session to the attacker's session context.
The official regression test suite included in the patch demonstrates the exact mechanics required to trigger the vulnerable response:
# Reproduction harness from activestorage/test/controllers/blobs/proxy_controller_test.rb
test "a session cookie under a rotated secret is not rewritten" do
# 1. Generate a legacy encrypted session cookie using an older secret
old_key_generator = ActiveSupport::KeyGenerator.new("old secret key base", iterations: 1000)
old_secret = old_key_generator.generate_key("authenticated encrypted cookie", 32)
session_key = Rails.application.config.session_options[:key]
old_cookie_jar = ActionDispatch::Request.new(
Rails.application.env_config.merge("action_dispatch.key_generator" => old_key_generator)
).cookie_jar
old_cookie_jar.encrypted[session_key] = { value: { "session_id" => SecureRandom.hex(16) } }
# 2. Configure the application to rotate the old encrypted secret
rotations = ActiveSupport::Messages::RotationConfiguration.new
rotations.rotate :encrypted, old_secret
# 3. Request the proxied blob with the legacy cookie
with_env_config("action_dispatch.cookies_rotations" => rotations) do
get rails_storage_proxy_url(create_file_blob(filename: "racecar.jpg")),
headers: { "Cookie" => "#{session_key}=#{Rack::Utils.escape(old_cookie_jar[session_key])}" }
end
# On vulnerable versions (< 8.1.4.1 / < 8.0.5.2), response.headers["Set-Cookie"] is populated.
# On patched versions, Set-Cookie is nil.
assert_response :success
assert_nil response.headers["Set-Cookie"]
endCVE-2026-126077 is assigned a CVSS v3.1 base score of 6.8 (Medium) with the vector CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N. The Attack Complexity is rated High (AC:H) because exploitation depends on specific server-side configurations (Active Storage proxy mode combined with active cookie rotation or hybrid serialization) and specific CDN/proxy caching behaviors (caching Set-Cookie on public responses). User Interaction is Required (UI:R) because either a victim carrying a legacy cookie must trigger a cache miss on a public asset, or a victim must visit a cache-poisoned asset URL.
When these prerequisites are met, the Confidentiality (C:H) and Integrity (I:H) impacts are High. In standard Rails configurations using ActionDispatch::Session::CookieStore, the session cookie either contains the entire session payload (encrypted and signed) or a session identifier referencing a server-side store (such as Redis or ActiveRecord). Disclosing the rewritten Set-Cookie header through a shared cache grants unauthorized actors full access to the victim's session state, allowing account takeover, viewing of private user data, and state-changing actions performed on behalf of the victim.
Furthermore, because Active Storage proxy controllers set a max-age of 100 years (3155695200 seconds) with the immutable directive, a poisoned cache entry can persist at the CDN edge indefinitely until manually purged or evicted, exposing the leaked session to every client requesting that file. As of November 2026, the FIRST EPSS score is 0.00415 (36.26th percentile), and the vulnerability is not listed in the CISA Known Exploited Vulnerabilities (KEV) catalog.
The primary remediation is to upgrade Ruby on Rails (rails, actionpack, and activestorage gems) to the patched maintenance releases: 8.1.4.1 for the 8.1 release series, or 8.0.5.2 for the 8.0 release series and earlier. Update the application's Gemfile and execute bundle update rails actionpack activestorage to apply the fix.
In addition to upgrading Rails, operators should implement defense-in-depth controls at the CDN or reverse proxy layer to ensure that Set-Cookie headers are never cached on public endpoints. While many modern CDNs bypass caching or strip Set-Cookie by default, custom caching rules or Nginx proxy_cache configurations often override default safety checks when Cache-Control: public is present.
For Nginx reverse proxies fronting Active Storage proxy routes, configure the location block to either refuse caching when Set-Cookie is present or strip the header entirely:
location /rails/active_storage/blobs/proxy/ {
proxy_pass http://rails_upstream;
# Prevent caching any response that contains a Set-Cookie header
proxy_no_cache $cookie_nocache $http_authorization $http_set_cookie;
# Strip Set-Cookie from upstream proxy responses
proxy_ignore_headers Set-Cookie;
proxy_hide_header Set-Cookie;
}If an immediate framework upgrade is not feasible, two application-level workarounds can eliminate the exposure:
# config/initializers/active_storage.rb
Rails.application.config.active_storage.resolve_model_to_route = :rails_storage_redirectconfig.action_dispatch.cookies_rotations or config.action_dispatch.cookies_serializer = :hybrid has completed, remove the rotation configuration and set the serializer to :json (note that any remaining unmigrated client cookies will be invalidated).> [!IMPORTANT]
> Organizations that were previously running in a vulnerable configuration should purge their CDN and reverse proxy caches for /rails/active_storage/blobs/proxy/* and /rails/active_storage/representations/proxy/* after patching to ensure no previously cached Set-Cookie headers remain at the edge.
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
actionpack / rails Ruby on Rails | >= 8.1.0, < 8.1.4.1 | 8.1.4.1 |
actionpack / rails Ruby on Rails | < 8.0.5.2 | 8.0.5.2 |
| Attribute | Detail |
|---|---|
| Vulnerability ID | CVE-2026-126077 / GHSA-x6mj-8jx9-qhqv |
| Weakness Class | CWE-200: Exposure of Sensitive Information to an Unauthorized Actor |
| CVSS v3.1 Score | 6.8 Medium (CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:N) |
| EPSS Score | 0.415% (36.26th Percentile) |
| Affected Component | ActionDispatch::Cookies & ActiveStorage::Blobs::ProxyController |
| Security Impact | Session Hijacking & Session Fixation via Shared Web Cache |
| Patched Versions | Rails 8.1.4.1 and 8.0.5.2 |
| CISA KEV Status | Not Listed |
The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information. Sensitive session cookies are attached via Set-Cookie to publicly cacheable responses, allowing shared intermediate web caches to record and distribute private session tokens to unauthorized requesters.
CVE-2026-126072 (GHSA-rwvh-fw6r-pwvp) is a concurrency deadlock vulnerability in the Ruby on Rails Action Pack framework affecting applications that utilize ActionController::Live response streaming. The defect occurs due to improper synchronization within ActionController::Live::Buffer when handling full bounded queues during stream closure and connection abortion. An unauthenticated remote attacker can trigger this condition by stalling TCP socket reads on streaming endpoints—including Active Storage proxy mode—resulting in permanent worker thread exhaustion and application-wide denial of service.
CVE-2026-126192 is an algorithmic complexity vulnerability in Soup Sieve (soupsieve), the default CSS selector engine for Beautiful Soup 4 in Python. Evaluating a CSS selector that contains a general sibling combinator (~) nested inside a :has() relational pseudo-class against attacker-controlled HTML markup triggers O(n^2) quadratic evaluation complexity when the right-hand sibling target is absent. An unauthenticated attacker can supply small HTML documents containing thousands of sibling elements to exhaust CPU resources and stall application worker threads.
CVE-2026-126190 (GHSA-mmhr-3c5m-f64f) is an inefficient algorithmic complexity vulnerability (CWE-407) in Soup Sieve, the default CSS selector library integrated into Beautiful Soup 4. In versions prior to 4.0, evaluating CSS selectors that contain descendant combinators (whitespace between compound selectors) executes an unmemoized upward traversal of the document tree. When evaluated against deeply nested HTML or XML documents, the time complexity scales polynomially at O(n^(k+1)), where n is the tree depth and k is the number of descendant combinators. Remote attackers can supply compact, deeply nested markup (1 KB to 4 KB) to trigger prolonged CPU starvation and denial of service in Python applications.
CVE-2026-122006 is a critical vulnerability in the Ruby on Rails Active Storage framework affecting versions prior to 8.0.5.2 and 8.1.4.1. When an application processes uploaded file attachments using the ImageMagick variant processor (:mini_magick), the framework invokes ImageMagick's metadata analyzer without applying a restrictive security policy. An unauthenticated attacker can upload a crafted file containing directives for unrestricted ImageMagick coders, such as Magick Scripting Language (MSL), resulting in arbitrary server-side file read and write operations. In deployments using the Disk storage service, attackers can exfiltrate sensitive application configuration files, including secret_key_base and master encryption keys, which enables remote code execution via signed payload forgery.
CVE-2026-128065 (also tracked as GHSA-gg5j-6742-v827) is an authorization bypass vulnerability affecting the @grpc/grpc-js-xds library in the Node.js gRPC ecosystem. In versions 1.14.0 and 1.14.1, XdsServer acknowledges Route and VirtualHost resources from an xDS control plane while failing to parse, store, or apply RBACPerRoute override configurations. When experimental RBAC is enabled and a broad top-level ALLOW policy is paired with restrictive VirtualHost or Route overrides, the server evaluates requests strictly against the top-level policy, permitting unauthorized RPC execution.
CVE-2026-126188 (GHSA-3f4q-8x5m-76v8) is an information disclosure vulnerability in the Active Storage component of Ruby on Rails prior to versions 8.0.5.2 and 8.1.4.1. When generating previews or extracting metadata from uploaded media files, Active Storage invokes the external command-line tools ffmpeg and ffprobe without specifying a format whitelist. An attacker who can upload a crafted HTTP Live Streaming (HLS) playlist while declaring a standard video or audio MIME type can cause FFmpeg to engage the HLS demuxer, resolve file:// segment URIs, and disclose visual frames or metadata from local media files readable by the Rails application process.