Nov 13, 2026·9 min read·1 visit
Active Storage configurations using Google Cloud Storage with the cache_control option generate V4 presigned upload URLs that fail to cryptographically bind the Content-Type and Content-MD5 headers. Attackers can exploit this omission to upload arbitrary HTML payloads and register malicious Service Workers under the application's bucket path on storage.googleapis.com.
A vulnerability classified under CWE-345 (Insufficient Verification of Data Authenticity) exists in the Active Storage component of Ruby on Rails prior to versions 8.1.4.1 and 8.0.5.2. When Google Cloud Storage (GCS) is configured as the storage backend with the cache_control option enabled, Active Storage switches from GCS V2 signing to V4 signing for direct upload URLs. Because the V4 signing implementation only binds the Cache-Control and Host headers, the Content-Type and Content-MD5 headers remain unsigned. An attacker can modify the Content-Type header during a direct upload request to store HTML or SVG files with active JavaScript on the GCS bucket origin, enabling Service Worker registration and interception of subsequent signed URL responses within the bucket path.
Active Storage is the built-in file upload and cloud storage integration framework in Ruby on Rails. It facilitates attaching files to Active Record models and provides a direct upload mechanism that allows client browsers to upload files directly to cloud storage providers—such as Amazon S3, Microsoft Azure Storage, and Google Cloud Storage (GCS)—bypassing the Rails application server during file transfer.
To secure direct uploads, the Rails application generates a cryptographically signed URL that grants temporary permission to write an object with specific constraints, such as file size, checksum, and MIME type. In ActiveStorage::Service::GCSService#url_for_direct_upload, the service constructs a presigned PUT URL utilizing the underlying Google Cloud Storage client library.
In Rails versions prior to 8.0.5.2 and 8.1.4.1, configuring the GCS storage service with the cache_control option altered the signing algorithm from GCS V2 to GCS V4. Under this specific V4 signing implementation, the cryptographic signature only covers the Cache-Control and Host HTTP headers, leaving the Content-Type and Content-MD5 headers unsigned and unverified by Google Cloud Storage.
This flaw is classified as CWE-345 (Insufficient Verification of Data Authenticity). Because GCS does not verify the authenticity of the Content-Type header during the upload transaction, an attacker can override the expected MIME type with text/html. When subsequent users access the uploaded object via GCS download URLs, the stored metadata takes precedence over query parameter overrides, resulting in stored Cross-Site Scripting (XSS) and Service Worker injection within the storage.googleapis.com bucket path.
The root cause of CVE-2026-126084 lies in the architectural differences between Google Cloud Storage V2 and V4 signing specifications, combined with how ActiveStorage::Service::GCSService constructs arguments for presigned URL generation. By default, Active Storage uses V2 signing (version = :v2) when generating direct upload URLs for GCS.
In the GCS V2 signing specification, the canonical string-to-sign explicitly includes positional fields for both Content-MD5 and Content-Type, even when they are empty:
PUT
<Content-MD5>
<Content-Type>
<Expires>
<CanonicalizedExtensionHeaders>
<CanonicalizedResource>However, GCS V2 signing does not support signing standard HTTP headers outside of the x-goog-* extension namespace. When support for custom Cache-Control headers was introduced to Active Storage's GCS service, developers conditionally switched the signing version to :v4 whenever @config[:cache_control].present? evaluated to true.
Under GCS V4 signing, headers are only included in the signature calculation if they are explicitly passed in the headers hash during URL generation. In url_for_direct_upload, Active Storage populated headers["Cache-Control"] and merged custom metadata headers, but did not pass Content-Type into the signed headers hash, nor did V4 enforce the content_md5 parameter in the same manner when omitted from the signed header list.
Furthermore, Google Cloud Storage exhibits a specific metadata precedence behavior during object retrieval. While Active Storage appends a response-content-type query parameter to signed download URLs to force safe content types or binary downloads, GCS ignores or overrides this parameter if an explicit Content-Type metadata value was stored directly on the object during the PUT upload. Consequently, the attacker-supplied Content-Type persists and dictates how the victim's browser renders the stored object.
The vulnerability resides in activestorage/lib/active_storage/service/gcs_service.rb within the url_for_direct_upload method. In the vulnerable implementation, the presence of @config[:cache_control] branches the execution path to use :v4 signing and populates the Cache-Control header in the headers hash.
# Vulnerable implementation in activestorage/lib/active_storage/service/gcs_service.rb
def url_for_direct_upload(key, expires_in:, checksum:, custom_metadata: {}, **)
instrument :url, key: key do |payload|
headers = {}
version = :v2
if @config[:cache_control].present?
headers["Cache-Control"] = @config[:cache_control]
# v2 signing doesn't support non `x-goog-` headers. Only switch to v4 signing
# if necessary for back-compat; v4 limits the expiration of the URL to 7 days
# whereas v2 has no limit
version = :v4
end
headers.merge!(custom_metadata_headers(custom_metadata))
args = {
content_md5: checksum,
expires: expires_in,
headers: headers,
method: "PUT",
version: version,
}
# ...Commits 8d93f6a872e9550a91bf0e4fce50083a235ef3b1 (Rails 8.1) and ddc08ba61a8001e7402864c62e466cf732fc7614 (Rails 8.0) remediate the issue by removing the conditional :v4 branch entirely and enforcing :v2 signing for all direct upload URLs.
--- a/activestorage/lib/active_storage/service/gcs_service.rb
+++ b/activestorage/lib/active_storage/service/gcs_service.rb
@@ -91,25 +91,14 @@ def exist?(key)
def url_for_direct_upload(key, expires_in:, checksum:, custom_metadata: {}, **)
instrument :url, key: key do |payload|
- headers = {}
- version = :v2
-
- if @config[:cache_control].present?
- headers["Cache-Control"] = @config[:cache_control]
- # v2 signing doesn't support non `x-goog-` headers. Only switch to v4 signing
- # if necessary for back-compat; v4 limits the expiration of the URL to 7 days
- # whereas v2 has no limit
- version = :v4
- end
-
- headers.merge!(custom_metadata_headers(custom_metadata))
-
+ # V2 signatures bind the Content-Type (empty) and Content-MD5 headers. GCS leaves headers
+ # other than x-goog-* out of V2 signatures, so the client sends Cache-Control unsigned.
args = {
content_md5: checksum,
expires: expires_in,
- headers: headers,
+ headers: custom_metadata_headers(custom_metadata),
method: "PUT",
- version: version,
+ version: :v2,
}By hardcoding version: :v2 and passing headers: custom_metadata_headers(custom_metadata), the generated signature cryptographically binds an empty Content-Type field and the Content-MD5 checksum. Meanwhile, headers_for_direct_upload still instructs the client to transmit the Cache-Control header during the PUT request. Because GCS V2 ignores non-x-goog-* headers during signature verification, the Cache-Control header is accepted unsigned while any attempt to inject a Content-Type header causes a signature mismatch and an HTTP 403 Forbidden response.
> [!NOTE]
> Because Cache-Control is now transmitted unsigned under V2 signing, a malicious client can alter the caching policy of the specific object they are uploading. However, this design trade-off completely eliminates the ability to spoof Content-Type or bypass Content-MD5 verification.
Exploiting CVE-2026-126084 requires a target Rails application that permits users to initiate direct uploads to a Google Cloud Storage backend configured with the cache_control parameter in config/storage.yml. The attacker does not need administrative privileges; any unauthenticated or authenticated endpoint that issues Active Storage direct upload tokens is sufficient.
In the first stage of the attack, the client requests a direct upload URL from the Rails server, declaring benign file metadata (e.g., filename: "avatar.png", content_type: "image/png"). The Rails application responds with a GCS V4 presigned PUT URL and the expected request headers.
In the second stage, instead of uploading a valid image, the attacker issues a direct HTTP PUT request to the presigned GCS URL containing an HTML document or JavaScript payload. In this PUT request, the attacker explicitly sets the Content-Type: text/html header (or text/javascript when uploading a Service Worker script) and omits or replaces the Content-MD5 header. Because V4 signing only validated Host and Cache-Control, GCS accepts the upload and records text/html as the permanent metadata for that storage object.
The official Rails regression test demonstrates the exact HTTP mechanics used to trigger the flaw (which returns 403 after the patch, but 200 OK on vulnerable versions):
key = SecureRandom.base58(24)
data = "<html></html>"
checksum = Digest::MD5.base64digest(data)
url = service.url_for_direct_upload(key, expires_in: 5.minutes, content_type: "image/png", content_length: data.size, checksum: checksum)
uri = URI.parse url
request = Net::HTTP::Put.new uri.request_uri
request.body = data
service.headers_for_direct_upload(key, checksum: checksum, filename: ActiveStorage::Filename.new("test.png"), content_type: "image/png").each do |k, v|
request.add_field k, v
end
# Attacker injects arbitrary Content-Type header
request.add_field "Content-Type", "text/html"
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
http.request request
endIn the final stage, the attacker uploads two objects: an HTML file and a JavaScript Service Worker payload (text/javascript). When a victim user navigates to the signed URL for the HTML file on https://storage.googleapis.com/<bucket-name>/<key>, the browser renders the HTML inline and executes its embedded script. The script invokes navigator.serviceWorker.register() scoped to https://storage.googleapis.com/<bucket-name>/, granting the attacker persistent interception capabilities over all subsequent requests made by that victim to the application's GCS bucket.
The CVSS v3.1 base score for CVE-2026-126084 is 4.7 (CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:L/I:L/A:N). The Attack Complexity is rated High (AC:H) because exploitation depends on a non-default server configuration (cache_control enabled on a GCS service) combined with active direct-upload functionality. User Interaction (UI:R) is required because a victim must visit the direct GCS URL for the uploaded HTML object to trigger script execution.
Crucially, the Scope is marked as Changed (S:C). Although the vulnerability originates in how the Rails application signs upload parameters, the resulting script execution occurs within the storage.googleapis.com web origin rather than the primary Rails application domain. Consequently, the attacker cannot directly read cookies or DOM state belonging to the main Rails application origin.
Despite being confined to the storage.googleapis.com origin, the security impact within the shared bucket namespace is significant. Because GCS path-style URLs share the origin https://storage.googleapis.com and segregate tenants by the /<bucket-name>/ path prefix, an attacker who registers a Service Worker under /<bucket-name>/ achieves persistence inside the victim's browser for that specific bucket.
Once the Service Worker is active, it intercepts all fetch events targeting https://storage.googleapis.com/<bucket-name>/*. This allows the attacker to exfiltrate sensitive private files accessed by the victim via signed download URLs (Confidentiality: Low) and serve tampered or spoofed files in place of legitimate bucket assets (Integrity: Low).
To remediate CVE-2026-126084, upgrade Ruby on Rails (specifically the rails and activestorage gems) to version 8.1.4.1 (for the 8.1 release series) or 8.0.5.2 (for the 8.0 release series). These versions enforce GCS V2 signing on all direct uploads, ensuring that Content-Type and Content-MD5 headers cannot be modified by the client.
Update the application's Gemfile to specify the patched release:
gem "rails", "~> 8.1.4.1" # Or "~> 8.0.5.2" for Rails 8.0.xThen execute Bundler to apply the updated gems:
bundle update rails activestorageIf an immediate framework upgrade is not feasible, administrators can apply a configuration workaround by removing or commenting out the cache_control key from the Google Cloud Storage definition in config/storage.yml:
google:
service: GCS
project: <%= ENV["GCS_PROJECT"] %>
bucket: <%= ENV["GCS_BUCKET"] %>
credentials: <%= ENV["GCS_CREDENTIALS"] %>
# Remove cache_control to force V2 signing on unpatched versions:
# cache_control: "public, max-age=3600"In addition to patching Rails, organizations that previously ran vulnerable configurations should audit their Google Cloud Storage buckets for existing objects with unexpected Content-Type metadata (such as text/html, image/svg+xml, or text/javascript) that do not match the content_type recorded in the Rails active_storage_blobs database table.
CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:L/I:L/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
activestorage (Ruby on Rails) Ruby on Rails | >= 8.1.0, < 8.1.4.1 | 8.1.4.1 |
activestorage (Ruby on Rails) Ruby on Rails | < 8.0.5.2 | 8.0.5.2 |
| Attribute | Detail |
|---|---|
| CVE ID | CVE-2026-126084 (GHSA-3w93-6hmw-whh4) |
| Weakness Class | CWE-345 (Insufficient Verification of Data Authenticity) |
| CVSS v3.1 Score | 4.7 Medium (CVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:L/I:L/A:N) |
| EPSS Score | 0.14% (3.38th Percentile) |
| Affected Component | ActiveStorage::Service::GCSService#url_for_direct_upload |
| Security Impact | Stored HTML/JS execution and Service Worker interception on GCS bucket path |
| Patched Versions | Rails 8.1.4.1, Rails 8.0.5.2 |
| CISA KEV Status | Not Listed |
The product does not sufficiently verify the origin or authenticity of data, in a way that causes it to accept invalid data.
A Cross-Site Scripting (XSS) vulnerability exists in Ruby on Rails Action View prior to versions 8.0.5.2 and 8.1.4.1. When applications use the default HTML5 sanitizer (Rails::HTML5::Sanitizer) with Nokogiri versions prior to 1.19.5, angle brackets (< and >) inside HTML attribute values are serialized unescaped. Action View text helpers—specifically highlight and simple_format—perform string-based manipulations on the sanitized HTML rather than operating on the DOM tree. An unescaped '>' character or a double newline ('\n\n') inside an attribute value causes these helpers to misidentify tag or paragraph boundaries, breaking out of the attribute context and emitting inert attribute contents as live HTML elements in the final html_safe output.
CVE-2026-126070 is a Cross-Site Scripting (XSS) vulnerability in the Action View component of Ruby on Rails. Prior to versions 8.0.5.2 and 8.1.4.1, the strip_tags helper returned sanitized output marked as html_safe without encoding single or double quotation marks. When applications render the output of strip_tags inside quoted HTML attributes in ERB templates, attackers can break out of the attribute context and inject arbitrary HTML attributes or JavaScript event handlers.
CVE-2026-126078 is a Cross-Site Scripting (XSS) vulnerability classified under CWE-79 affecting the Action View component of Ruby on Rails in versions prior to 8.0.5.2 and 8.1.4.1. The vulnerability resides in the ActionView::Helpers::TextHelper#highlight method when invoked with a custom :highlighter template that places capture group placeholders inside HTML attribute values. Because matched substrings are substituted without HTML escaping after initial sanitization preserves literal quotation marks, an attacker controlling the input text or target search phrase can break out of the surrounding attribute delimiter and inject arbitrary HTML attributes or JavaScript event handlers.
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.
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.