Skip to content

CVE-2026-66066: Verified Reproduction

CVE-2026-66066: Rails Active Storage variant processing arbitrary file read and potential RCE

CVE-2026-66066 is verified against rails/rails · github. Affected versions: activestorage < 7.2.3.2 (Rails 7.0.0-7.2.3.1 affected in default config); 8.0.0-8.0.5; 8.1.0-8.1.3; Rails 6.x only with non-default Active Storage config. Vulnerability class: RCE. This critical reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00317.

REPRO-2026-00317 rails/rails · github RCE Aug 1, 2026 CVE entry ↗ .txt
Severity
CRITICAL
Confidence
HIGH
Reproduced in
148m 42s
Tool calls
356
Spend
$17.09
01 · Overview

What Is CVE-2026-66066?

CVE-2026-66066 is a critical-severity RCE vulnerability affecting rails/rails activestorage < 7.2.3.2 (Rails 7.0.0-7.2.3.1 affected in default config); 8.0.0-8.0.5; 8.1.0-8.1.3; Rails 6.x only with non-default Active Storage config. Pruva has independently reproduced it and publishes a verified, runnable proof-of-concept (reproduction REPRO-2026-00317).

02 · Severity & CVSS

CVE-2026-66066 Severity

CVE-2026-66066 is rated critical severity.

CRITICAL threat level
Weakness CWE-1188

Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.

03 · Affected Versions

Affected rails/rails Versions

rails/rails · github versions activestorage < 7.2.3.2 (Rails 7.0.0-7.2.3.1 affected in default config); 8.0.0-8.0.5; 8.1.0-8.1.3; Rails 6.x only with non-default Active Storage config are affected.

How to Reproduce CVE-2026-66066

$ pruva-verify REPRO-2026-00317
or curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00317/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh
Run in a VM or disposable container. This exploits a real vulnerability.
06 · Proof of Reproduction

Proof of Reproduction for CVE-2026-66066

Remote code execution — reproduced
  • reached the target end-to-end
  • full exploit chain demonstrated
  • on the real production code path
  • high confidence
  • the upstream fix blocks the same trigger
Trigger

crafted .mat (HDF5) file uploaded via POST /rails/active_storage/direct_uploads + PUT to the disk-service token URL, declared as image/png; plus a signed variation token scraped from a public page and, for the RCE stage, an offline-forged variation token minted with the leaked secret_key_base

Attack chain
  1. GET /rails/active_storage/representations/redirect/:signed_blob_id/:variation_key/:filename
  2. ActiveStorage::Variant#process
  3. ImageProcessing::Vips
  4. Vips::Image.new_from_file
  5. libvips matload (untrusted)
  6. matio/HDF5 external storage reads /proc/self/environ; RCE stage: same endpoint with forged {"instance_eval": "<ruby>"} variation token
How the agent worked 829 events · 356 tool calls · 2h 29m
2h 29mDuration
356Tool calls
181Reasoning steps
829Events
32Dead-ends
Agent activity over 2h 29m
Policy
1
Support
16
Repro
524
Judge
37
Variant
140
Coding
105
Verify
1
0:00148:33

Root Cause and Exploit Chain for CVE-2026-66066

Versions: activestorage < 7.2.3.2, >= 8.0 < 8.0.5.1, >= 8.1 < 8.1.3.1,

Rails Active Storage's :vips variant processor (the default since Rails 7.0) passes attacker-uploaded files to libvips without disabling libvips' "untrusted" (unfuzzed) operations. On the standard Debian/Ubuntu libvips build — the same build shipped by the official ruby Docker images and installed by rails new generated Dockerfiles — the untrusted matload operation (matio + HDF5) is available. An unauthenticated attacker can upload a crafted MATLAB v7.3 (.mat/HDF5) file whose matrix data lives in HDF5 external storage segments pointing at an arbitrary absolute path on the server (e.g. /proc/self/environ). When Active Storage generates an image variant from the upload, libvips loads it with matload, and HDF5/matio transparently read the referenced file, returning its bytes as image pixels. The processed variant is served back to the attacker, yielding an arbitrary file read as the Rails process user, including the process environment with SECRET_KEY_BASE. Active Storage 8.0.5.1 / 7.2.3.2 / 8.1.3.1 fix this by calling Vips.block_untrusted(true) at boot (requiring libvips >= 8.13 and ruby-vips >= 2.2.1).

  • Package/component: activestorage (Ruby on Rails) variant processing via ruby-vips/image_processing (config.active_storage.variant_processor = :vips, default with load_defaults 7.0 and later).
  • Affected versions: activestorage < 7.2.3.2, >= 8.0 < 8.0.5.1, >= 8.1 < 8.1.3.1, with libvips linked against certain third-party libraries (Debian/Ubuntu default builds include matio/HDF5, ImageMagick, poppler, librsvg).
  • Risk level: critical. Unauthenticated arbitrary file read of any file the Rails process can read (environment secrets, credentials, other users' data). The advisory notes these secrets (especially secret_key_base) may enable remote code execution or lateral movement.

Impact Parity

  • Disclosed/claimed maximum impact: remote code execution (via arbitrary file read -> secret_key_base -> RCE/lateral movement).
  • Reproduced impact in this run: unauthenticated remote code execution through the production HTTP path, chained as:
    1. arbitrary file read — the Rails process environment (/proc/self/environ) is exfiltrated byte-exactly through the app's own resize_to_limit: [100, 100] variant, leaking SECRET_KEY_BASE (canary recovered, two independent attempts);
    2. forged signed variation tokens — the leaked secret replicates Rails.application.message_verifier("ActiveStorage") offline and mints a variation token carrying an unvalidated {"instance_eval": "<ruby>"} transformation (the :vips ImageProcessingTransformer performs no transformation validation; rails issue #56948);
    3. code execution — delivering the forged token via GET /rails/active_storage/representations/redirect/<signed blob id>/<token>/pwn.png executes the Ruby in the Puma process (unique on-disk markers written in three fresh vulnerable processes; wrong-key control: 404, no marker; fixed 8.0.5.1: chain broken at step 1).
  • Parity: full — unauthenticated RCE on a default-configured app (variant_processor :vips, image_processing 1.x, untrusted uploads with variants displayed), matching the advisory's claimed maximum impact.

Root Cause

  1. Missing hardening call. Before the fix, Active Storage never called Vips.block_untrusted(true) (libvips >= 8.13) nor set VIPS_BLOCK_UNTRUSTED, so every libvips loader/saver flagged VIPS_OPERATION_UNTRUSTED ("unfuzzed") remained reachable for attacker-controlled uploads. libvips selects loaders by content sniffing, so the web-facing declared MIME type (blob.content_type in variable_content_types) does not constrain which loader actually parses the bytes.

  2. A loader that dereferences server-side paths. The untrusted VipsForeignLoadMat (matload, via matio) reads MATLAB files; v7.3 .mat files are HDF5. HDF5 datasets may keep their payload in external storage segments (H5Pset_external(name, offset, size)), where name may be an absolute path. matio/HDF5 resolve and read those segments transparently, so a crafted dataset's pixel values become the raw bytes of an arbitrary server file. Details that make the payload viable:

    • libvips vips__mat_ismat() only accepts files starting with MATLAB 5.0 (text prefix at offset 0);
    • matio's Mat_Open() ignores the descriptive text and decides v7.3/HDF5 purely from the header version field 0x0200 and endian indicator (IM on disk for little-endian), then calls H5Fopen (the HDF5 signature lives after the 512-byte user block);
    • matio requires a MATLAB_class attribute stored as a fixed-size, NUL-padded ASCII string.
  3. End-to-end exfil channel. Active Storage's unauthenticated flow (direct upload -> representations URL) lets the attacker have a variant generated for their own blob:

    • POST /rails/active_storage/direct_uploads returns a signed_id for the crafted blob (declared image/png; no content verification at upload). DiskController skips CSRF protection for the subsequent PUT.
    • Variation URL tokens (ActiveStorage.verifier.generate(transformations, purpose: :variation)) sign only the transformation hash, not any blob id, so a token scraped from any public page that renders an image variant can be replayed against the attacker's own signed_id.
    • RepresentationsController#show synchronously processes the variant on first request and redirects to the stored PNG.
    • The ImageProcessing vips pipeline applies a sharpen convolution ([-1,-1,-1; -1,32,-1; -1,-1,-1]/24) after thumbnailing. The payload replicates each target byte into three consecutive pixels using one 1-byte external-storage segment per pixel (3 segments per file byte) and a 99x1 matrix, so (a) no resampling occurs under resize_to_limit: [100, 100], and (b) each triple's center pixel has all-equal 3x3 neighbours and survives the convolution byte-exactly.

    One quirk matters for real apps: if the crafted blob is attached to a record, the analyzer rewrites blob.content_type to the sniffed application/x-matlab-data, after which blob.variable? is false and variants are refused (ActiveStorage::InvariableError). Orphan blobs created by direct upload are never analyzed, so the declared image/png stands — this is the path the exploit uses, and it requires no attachment.

  4. The fix. activestorage/lib/active_storage/vips.rb (new in 8.0.5.1) loads ruby-vips at boot and calls Vips.block_untrusted(true), raising at boot when libvips < 8.13 or ruby-vips < 2.2.1 ("unsecurable environment"). With it, matload (and svgload, pdfload, magickload, ...) raise Vips::Error: matload: operation is blocked — exactly the behavior the fixed build shows in this reproduction. Fix commit range: v8.0.5...v8.0.5.1 (also 7.2.3.2, 8.1.3.1).

Reproduction Steps

  1. bundle/repro/reproduction_steps.sh (self-contained; run with PRUVA_ROOT=<bundle> or from bundle/repro/).
  2. The script:
    • installs ruby (>= 3.2), libvips-dev/tools, python3-h5py, sqlite dev headers, bundler;
    • asserts the environment precondition: libvips >= 8.13 with matload present and flagged untrusted (Debian/Ubuntu default build);
    • generates a minimal but realistic Rails app twice — rails/activestorage 8.0.5 (vulnerable) and 8.0.5.1 (fixed) — with variant_processor = :vips, Disk service, an unauthenticated upload form page (CSRF meta tag + a sample image variant URL), and SECRET_KEY_BASE sourced from the process environment containing a canary value;
    • boots each app with Puma (two clean attempts per build);
    • per attempt, as an unauthenticated attacker: GET / (session + CSRF token + scrape signed variation token), generate the .mat payload targeting /proc/self/environ at increasing offsets, direct-upload each payload (declared image/png), replay the scraped variation token against the attacker's own signed blob id, download the served variant PNG, and decode the exfiltrated bytes;
    • asserts the vulnerable builds leak SECRET_KEY_BASE=KINDARAILS2SHELL_... and the fixed builds fail closed with Vips::Error (matload: operation is blocked).
  3. Expected evidence: RESULT: VULNERABILITY CONFIRMED with vulnerable leaks=2/2, fixed blocked=2/2, exit code 0.

Evidence

  • bundle/logs/reproduction_steps.log — full run log; key excerpts:
    • [deps] vips-8.14.1 / libvips matload present and marked untrusted
    • * activestorage (8.0.5) vs * activestorage (8.0.5.1), image_processing (1.14.0), ruby-vips (2.3.0)
    • [vuln 1] canary SECRET_KEY_BASE recovered from /proc/self/environ followed by the leaked environment, containing SECRET_KEY_BASE=KINDARAILS2SHELL_CANARY_... (both attempts)
    • [fixed 1/2] variant processing blocked: Vips::Error (matload: operation is blocked
  • bundle/logs/attempts/vuln_*/leaked_all.raw — raw exfiltrated /proc/self/environ bytes (decoded from the served variant PNGs).
  • bundle/logs/attempts/vuln_*/server.log, fixed_*/server.log — Puma/Rails logs of both builds.
  • bundle/logs/attempts/vuln_*/chunk_*/du.json, pwn.png — per-chunk direct upload responses and served variant images.
  • bundle/repro/runtime_manifest.json — runtime manifest (entrypoint endpoint, service/health/target all true).
  • Environment: Debian bookworm (ruby:3.4-bookworm container), ruby 3.4.10, libvips 8.14.1 (matio/HDF5 build, matload untrusted), matio 1.5.21, HDF5 1.10, no sanitizers (production-path proof).

Recommendations / Next Steps

  • Upgrade to activestorage 7.2.3.2 / 8.0.5.1 / 8.1.3.1 (or later) and libvips >= 8.13; rotate secret_key_base and any credentials present in the application environment.
  • Stopgap on libvips >= 8.13: set VIPS_BLOCK_UNTRUSTED=1 or call Vips.block_untrusted(true) in an initializer; on libvips < 8.13 remove the ruby-vips dependency entirely.
  • The same hardening should be considered defense-in-depth for any product that runs libvips on untrusted content without block_untrusted.
  • Escalation artifacts: bundle/repro/escalation_experiments.sh (runnable), bundle/logs/escalation/escalation.log, rce_marker.txt, rce_marker2.txt (unique markers written by injected code inside fresh Puma processes), negative_control.json + srvNC.log (wrong-key control, HTTP 404, no marker), logs/escalation/leak/ (full-environ leak used to recover the complete secret). The second-stage instance_eval transformation injection relies on the :vips transformer applying no transformation validation (rails issue #56948); it is only reachable to an unauthenticated attacker because variation tokens are signed and the CVE-2026-66066 file read yields the signing secret.
  • Note for testing: image_processing 2.x independently calls Vips.block_untrusted(true) when it loads; pin observations accordingly when evaluating exploitability (this reproduction pins 1.14.0 so the only variable is the activestorage version).

Additional Notes

  • Idempotency: the script resets each app's DB/storage per attempt and was run twice consecutively in a fresh container; both runs passed (vulnerable leaks=2/2, fixed blocked=2/2).
  • Other untrusted loaders on Debian/Ubuntu libvips (svgload, pdfload, openslideload, magickload, jxlload, jp2kload, fitsload, openexrload, analyzeload, radload, ppmload, csvload, rawload, vipsload, matload) are additional candidate vectors; the ImageMagick route is heavily restricted on Debian/Ubuntu by the @* path policy, MVG/MSL stealth registration and StrictReadImage nested-coder blocking, while the matload/HDF5 route used here works on the default build. The upstream disclosure notes the reported chain may differ; any single untrusted loader suffices to prove the CVE.
  • The chunk size (33 bytes/request) is an artifact of defeating the sharpen convolution under resize_to_limit: [100, 100]; larger chunks are possible with larger variant limits or format-only variants.
  • The escalation was additionally validated with negative controls: a forged token signed with a wrong key is rejected (HTTP 404, no marker), and the fixed app (8.0.5.1) blocks the initial file read (matload: operation is blocked), leaving the signing key unobtainable. See bundle/learning/exploit_escalation.json (outcome: demonstrated) and bundle/repro/exploit_knowledge.json for the recorded primitives and the derived command-execution capability.

CVE-2026-66066 Reproduction Transcript

The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.

Full session Replay every step — scrub the timeline or play it back.

Event 1/40
0:005:23
0:00
session startedaccounts/fireworks/models/kimi-k3 · CVE-2026-66066 · REPRO-20
0:10
1:01
1:03
web search
1:12
1:13
1:15
web search
1:27
1:28
2:16
2:16
extract_facts
no facts extracted
2:19
2:19
supportclaim_contract
2:34
2:35
2:35
2:44
2:44
2:44
2:44
2:57
2:59
web search
3:00
$ls /pruva/project-cache/ 2>/dev/null; which ruby; ruby -v 2>/dev/null; which docker; gem --version 2>/dev/null
0.4s
{"cwd":"/workspace","duration_ms":49,"exit_code":127,"stderr":"","stdout":"repo-mirrors\n/usr/bin/docker\n","success":false,"timeout_secs":120}
3:00
error

Unknown error

4:21
4:22
4:26
web search
5:22
5:23
08 · How to Fix

How to Fix CVE-2026-66066

Coming soon

Step-by-step mitigation and hardening guidance for CVE-2026-66066 — configuration checks, workarounds where no patch exists, and how to verify you're protected — is on the way.

10 · FAQ

FAQ: CVE-2026-66066

Is CVE-2026-66066 exploitable?

Yes. Pruva independently reproduced CVE-2026-66066 in rails/rails and verified the exploit fires end-to-end in a sandboxed environment. A runnable proof-of-concept script and the full agent transcript are on this page (reproduction REPRO-2026-00317).

How severe is CVE-2026-66066?

CVE-2026-66066 is rated critical severity.

What type of vulnerability is CVE-2026-66066?

CVE-2026-66066 is classified as CWE-1188, a RCE vulnerability.

Which versions of rails/rails are affected by CVE-2026-66066?

rails/rails activestorage < 7.2.3.2 (Rails 7.0.0-7.2.3.1 affected in default config); 8.0.0-8.0.5; 8.1.0-8.1.3; Rails 6.x only with non-default Active Storage config is affected by CVE-2026-66066.

How can I reproduce CVE-2026-66066?

Pruva provides a verified reproduction script on this page. Download it and run it inside an isolated environment such as a container or virtual machine — never against production. The reproduction was confirmed end-to-end by Pruva's automated agents.

Is the CVE-2026-66066 reproduction verified?

Yes. Pruva reproduced CVE-2026-66066 with high confidence in a sandboxed environment, capturing the full agent transcript and artifacts as evidence.
11 · References

References for CVE-2026-66066

Authoritative sources for CVE-2026-66066 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.