Rapid7 Analysis: KindaRails2Shell (CVE-2026-66066)
Overview On July 29, 2026, the Ruby on Rails project published a security advisory for CVE-2026-66066 , an arbitrary file read in Active Storage applications that use the Vips image processor with untrusted uploads. The affected Active Storage ranges are < 7.2.3.2 , >= 8.0, < 8.0.5.1 , and >= 8.1, < 8.1.3.1 . Vips is the default Active Storage variant processor for applications that load Rails 7.0 or later defaults. Rails 6 applications are affected only when they explicitly configure Vips. Our Emergent Threat Response blog covers the affected versions, mitigation guidance, and current exploitation status. This post traces the request from the direct-upload endpoint to the HDF5 read, then shows how the arbitrary file read can expose Rails signing material and become code execution. A vulnerable application can disclose arbitrary files before the attacker has recovered a Rails secret or forged a token. A genuine Active Storage variation_key from the same application, paired with a direct-upload blob whose stored content_type claims to be an image, is enough to reach a libvips loader that turns a crafted MAT/HDF5 file into an arbitrary file-read oracle. We reproduced the published chain against Rails 6.0.6.1 , 6.1.7.10 , 7.2.3.1 , 8.0.5 , and 8.1.3 , and confirmed that patched 7.2.3.2 , 8.0.5.1 , and 8.1.3.1 targets block the crafted representation. We also validated a remote code execution (RCE) path that uses only JSON-compatible Hash , Array , and String values in a signed variation. That path reaches Kernel#spawn or Kernel#eval through ImageProcessing's chain builder, and it worked when Rails was configured with config.active_support.message_serializer = :json . The advisory covers the vulnerable Active Storage configuration. The MAT/HDF5 representation chain shown here has narrower requirements. The deployed libvips build must expose matload with MAT 7.3/HDF5 support, the application must preserve an attacker-supplied content_type , and the attacker must be able to trigger a representation, for example with a genuine variation key. Those requirements narrow where this particular chain works, but the underlying issue is that Active Storage handed untrusted uploads to libvips operations that libvips already marked unsafe for untrusted content. The attack can be summarized as follows: [Attacker] | | 1. Creates a direct-upload blob with content_type = image/png v [Rails stores the blob as an image without examining the bytes] | | 2. Reuses a genuine variation_key from the same application v [Rails accepts the blob as variable and starts a representation] | | 3. image_processing hands the local tempfile path to libvips v [libvips matload] | | 4. Bytes 0-9 match "MATLAB 5.0" v [libmatio] | | 5. Bytes 124-125 contain MAT_FT_MAT73 (0x0200) v [HDF5 external storage] | | 6. Dataset bytes come from attacker-chosen path + offset v [Rendered PNG representation] | --> Target file bytes are returned as image pixels Analysis The published chain contains two separate trust failures. Rails decides that a blob is an image from a database value, while libvips decides what parser to use from the bytes on disk. Once the file reaches matload , libvips and libmatio disagree again about the same MAT header. libvips only looks at the first ten bytes, while libmatio selects the MAT version from bytes 124 and 125. Direct upload stores an attacker-controlled type The standard direct-upload endpoint creates the blob record before the service receives the file. In Rails 8.0.5 , ActiveStorage::DirectUploadsController#create accepts content_type directly from the request and passes it into create_before_direct_upload! : class ActiveStorage::DirectUploadsController < ActiveStorage::BaseController def create blob = ActiveStorage::Blob.create_before_direct_upload!(**blob_args) # <-- [1] render json: direct_upload_json(blob) end private def blob_args params.expect(blob: [:filename, :byte_size, :checksum, :content_type, metadata: {}]).to