CVE-2026-66066 (KindaRails2Shell): Rails Active Storage Arbitrary File Read to RCE, Full Chain Now Public
TL;DR
  • CVE-2026-66066 is a CVSS v4 9.5 critical flaw in Rails Active Storage. An unauthenticated attacker uploads a crafted file and reads arbitrary files from the application server, including secret_key_base.
  • Patching is not the whole job. Rails explicitly tells affected operators to rotate secret_key_base, the master key, database credentials, storage service keys, and third-party tokens.
  • Exploitation barrier is gone. Rails published forensic tooling and technical details on July 30-31, ahead of the planned August 28 date. Rapid7's advisory records a full Rapid7 Labs root cause analysis with an accompanying Metasploit module added August 3, 2026.
  • Two version bumps required. Active Storage 7.2.3.2 / 8.0.5.1 / 8.1.3.1, and libvips must be 8.13 or later or patched Rails will refuse to boot.

What happened

On July 29, 2026 the Ruby on Rails project published advisory GHSA-xr9x-r78c-5hrm for CVE-2026-66066, rated critical at CVSS v4 9.5 and classified CWE-1188, Initialization of a Resource with an Insecure Default.

The affected component is Active Storage variant processing. Rails shipped fixes in activestorage 7.2.3.2, 8.0.5.1, and 8.1.3.1.

The flaw was reported independently by 0xacb, s3np41k1r1t0 and castilho of Ethiack, and by RyotaK of GMO Flatt Security. Ethiack named the chain KindaRails2Shell.

Timeline

Date (2026) Event
Jul 21 Ethiack builds initial working PoC
Jul 22 Reported to Rails
Jul 29 Fixes shipped, CVE-2026-66066 assigned, advisory published
Jul 30 Rapid7 publishes its Emergent Threat Response advisory
Jul 30-31 Rails publishes technical details and forensic tooling early after third-party PoCs appear
Aug 3 Rapid7 adds a Labs root cause analysis with an accompanying Metasploit module

Ethiack dates the Rails technical disclosure to July 30; Rapid7 records it as July 31. The one-day discrepancy does not change the operational picture.

Technical root cause

libvips loads far more than web images

Since Rails 7.0 defaults, config.active_storage.variant_processor is :vips. The official Rails Docker images install libvips on a Debian base. Depending on build flags, libvips can open PDFs, SVG, FITS astronomy images, NIfTI medical scans, OpenSlide pathology slides, and MATLAB .mat files. libvips selects a loader by sniffing bytes, not by trusting a filename or declared MIME type.

libvips already flags several of these loaders as unfuzzed and unsafe for untrusted content, and exposes a switch to block them. Per the Rails advisory, Active Storage did not set that switch. That is the entire defect.

The HDF5 external dataset primitive

A MAT v7.3 file is an HDF5 container. HDF5 supports external storage via H5Pset_external: a dataset's raw bytes can live in another file, at an arbitrary path and offset. Ethiack found that libmatio, the MATLAB library libvips links against, never calls H5Pget_external_count() before H5Dread. So reading a variable performs an intended arbitrary file read of whatever path the attacker embedded.

The format disagreement that makes it reachable

This is the part worth internalizing. libvips's sniffer vips__mat_ismat only accepts files whose header begins with the literal ASCII string MATLAB 5.0. libmatio dispatches on the 2-byte version word at offset 124: 0x0200 means use the HDF5 backend.

So the payload lies in exactly one place. Offset 0 says MATLAB 5.0 so libvips routes it to libmatio. Offset 124 says 0x0200 so libmatio parses it as HDF5 and follows the external reference. One file, two parsers, two different conclusions.

Content-type confusion in Active Storage

Active Storage's Blob#variable? checks the declared content_type string against an allowlist. On the direct-upload path, ActiveStorage::DirectUploadsController#create calls create_before_direct_upload!, which writes the client-supplied content_type into the database as-is, without re-identifying the bytes. An attacker declares image/png, uploads MAT bytes, and Rails treats the blob as an image.

Rapid7 tested this and found ordinary server-side attachment does not satisfy the chain, because Rails re-identifies the file as MATLAB data before variant processing. The direct-upload route is the reachable path, and it is mounted by default whenever Active Storage routes are mounted, even if your own UI never uses direct uploads. That default is the detail most teams will miss.

Why it is unauthenticated

The representations route uses a signed variation_key. Ethiack's key finding: the variation key signs only the transform, not the blob. It is blob-independent and replayable. An attacker harvests any legitimate representation URL from page HTML, an og:image tag, an API response, or a Wayback Machine snapshot, then replays that key against the signed blob id they just uploaded.

Resize transforms interpolate neighbouring pixels and corrupt a straight byte read, so the read is done one byte per request with a 1x1 image, which has no neighbours to blend. Reads past end-of-file return zero-fill, which conveniently reveals file length.

Escalation to code execution

With arbitrary read, the attacker pulls config/master.key and config/credentials.yml.enc, or reads /proc/self/environ for SECRET_KEY_BASE. Holding secret_key_base, they derive the Active Storage verifier key and forge their own variation keys. Ethiack notes the vips transformer inherits a base validation that only blocks combine_options, leaving the method allowlist unenforced, so a forged transform reaches Vips::Image.public_send(name, *args). Rapid7 independently verified an RCE escalation using recovered signing material to forge an ImageProcessing 1.x variation, without Marshal deserialization.


What this means for security teams

Three things separate this from a routine patch cycle.

First, the blast radius is credential-shaped, not host-shaped. If the read fired, your S3 keys, database credentials, and third-party API tokens left the building. Patching does not un-exfiltrate them.

Second, the exposure is inherited, not chosen. Most affected teams did not decide to run libvips or to expose a direct-upload endpoint. Both came from framework and container defaults. Ask your teams which apps accept an avatar upload, then work backwards. This is the same discovery gap discussed in the CISOPlatform fireside on managing a dynamic attack surface with Rick Doten.

Third, timelines compressed. Ethiack reports the primitive was found by pointing an LLM at libvips loader source for roughly 24 hours, and a second researcher independently reproduced a similar chain days later. Rails moved its disclosure date forward by nearly four weeks because public PoCs appeared anyway. If your remediation SLA assumes weeks of grace between advisory and weaponization, that assumption is no longer load-bearing. Our earlier note on writing patching policy as percentages rather than deadlines is relevant here.

If you do one thing today: inventory every Rails application that accepts user image uploads and confirm which activestorage version it runs. Version inventory before patch scheduling.

Remediation, in order

  1. Upgrade activestorage to 7.2.3.2, 8.0.5.1, or 8.1.3.1 or later. The advisory lists affected ranges as < 7.2.3.2, >= 8.0 < 8.0.5.1, and >= 8.1 < 8.1.3.1.
  2. Upgrade libvips to 8.13 or later. Rails alone is insufficient. Patched Active Storage raises an exception at boot if libvips cannot disable unfuzzed operations. Treat a boot failure after upgrade as a signal, not a bug.
  3. Upgrade ruby-vips to 2.2.1 or later where it is installed.
  4. Apply the stopgap if you cannot upgrade now. On libvips 8.13+, set the VIPS_BLOCK_UNTRUSTED environment variable, or call Vips.block_untrusted(true) from an initializer with ruby-vips 2.2.1+. On libvips below 8.13, Rails states the only workaround is removing the libvips dependency. Applications that pull in ruby-vips only for image analysis may be able to drop it from the Gemfile.
  5. Rotate every secret readable by the application process: secret_key_base, the master key in config/master.key or RAILS_MASTER_KEY, everything inside config/credentials.yml.enc, Active Storage service credentials for S3, GCS, or Azure, database credentials, and third-party tokens. Rails is explicit that rotation with the old secret retained as a fallback is only an intermediate step. Plan for the side effects: changing secret_key_base expires active sessions, and invalidates encrypted cookies, signed cookies, signed global IDs, and Active Storage URLs.
  6. Run the Rails forensic toolkit published at rails/rails-forensics-CVE-2026-66066 to determine whether you were vulnerable and to search Active Storage data for crafted files. Do this early. Rapid7 warns that scheduled cleanup of unattached blobs can destroy the evidence.
  7. Do not treat a WAF as the fix. Akamai shipped WAF protections and coordinated with Ethiack, but Ethiack's own guidance is that a WAF buys time in some deployments and that attackers with AI tooling can reconstruct the chain from patch diffs.

Detection ideas

  1. Direct-upload blob metadata. Query active_storage_blobs for rows where the declared content_type is an image type but the stored bytes do not begin with a matching magic number. A crafted payload starts with the ASCII string MATLAB 5.0.
  2. HDF5 superblock after the MAT header. Scan stored blobs for the byte sequence \x89HDF\r\n\x1a\n, which Ethiack places at offset 512 behind a 512-byte userblock. In a file claiming to be a PNG or JPEG, that is not ambiguous.
  3. Repeated representation requests with one variation key. The byte-at-a-time read produces a large volume of GET /rails/active_storage/representations/redirect/<blob_id>/<variation_key>/ requests reusing a single variation key across many distinct blob ids. Alert on high-cardinality blob ids paired with a low-cardinality variation key in your reverse proxy or CDN logs.
  4. Variant worker errors. Look in your job queue logs for Vips::Error spikes and for image processing jobs touching paths outside the upload directory.
  5. Post-patch boot failures. A patched Rails app that will not start is telling you libvips is below 8.13. Route those to the vulnerability queue, not the deploy queue.
  6. Egress from image workers. Variant processing containers should not be making outbound connections. Anything past that baseline warrants a look.

Immediate action checklist

  1. List Rails applications accepting untrusted image uploads, including internal tools and vendor-supplied products built on Rails.
  2. Record activestorage version, libvips version, ruby-vips version, and variant_processor setting per application.
  3. Confirm whether Active Storage routes are mounted, which exposes /rails/active_storage/direct_uploads by default.
  4. Patch activestorage and libvips together, in the same change.
  5. Run the Rails forensic toolkit before any unattached-blob cleanup job runs.
  6. Open a secret rotation ticket per application with an owner and a date, separate from the patch ticket.
  7. Ask third-party vendors running Rails whether they have patched and rotated. Ethiack confirmed the chain end-to-end against multiple shipped products in default configuration.

Community Note

The recurring pattern in this incident is that nobody chose the exposure. The vips processor, the libvips build linked against libmatio, and the mounted direct-upload route all arrived as defaults. That is exactly the class of exposure an asset inventory does not surface, because the asset is known and the reachable path is not. For teams wanting to validate what is actually reachable from outside, FireCompass offers a free self-serve AI pen test through Free Explorer, with no asset list and no credit card required.

Related on CISOPlatform


Join the Discussion

Three questions for practitioners who have worked this one:

  1. How are you handling the secret rotation half? Forced session expiry across a large user base is an availability conversation, not just a security one. Did you rotate immediately or stage it behind a maintenance window?
  2. Did any of your Rails apps fail to boot after patching because libvips was below 8.13, and how many of those did you not know were running an old libvips?
  3. Are you inventorying reachable routes or only deployed packages? The direct-upload endpoint being mounted by default is the kind of thing an SBOM will not tell you.

CISOPlatform is a free community of senior security leaders: breach postmortems, playbooks, and peer discussions. Join free: https://www.cisoplatform.com/main/authorization/signUp

Browse the CISO fireside chat archive for past sessions with practicing CISOs.

Sources

Votes: 0
E-mail me when people leave their comments –

Priyanka Aash is Co-Founder of CISO Platform, the world's first online community for information security executives, and Co-Founder of FireCompass. She has been nominated for the Cybersecurity Excellence Award for leadership and AI innovation in cybersecurity, honored with the NetApp Excellerate HER award, and featured in SC Media's Women in IT Security series. She is the author of The AI Divide. Security technologist Bruce Schneier advises FireCompass.

You need to be a member of CISO Platform to add comments!

Join CISO Platform

Join The Community Discussion