Your image upload thumbnail generator is an open door to every secret on the server
CVE-2026-66066, dubbed KindaRails2Shell by its discoverers at Ethiack, is a pre-authentication arbitrary file read chained to remote code execution in Ruby on Rails Active Storage. The flaw lives in how Active Storage hands untrusted uploads to libvips without disabling its "unfuzzed" format loaders — handlers for formats like MATLAB/HDF5 that were never security-hardened. An attacker uploads a crafted file to any endpoint that triggers variant generation (thumbnails, avatars, previews), and the unfuzzed loader reads arbitrary files as the Rails worker process. The primary targets are SECRET_KEY_BASE, config/master.key, database credentials, and cloud storage tokens. With SECRET_KEY_BASE in hand, the attacker forges a signed Ruby Marshal deserialization payload to achieve full remote code execution. Affected versions: activestorage < 7.2.3.2, 8.0.0–8.0.5.0, 8.1.0–8.1.3.0, and Rails 6.x only where variant_processor = :vips was explicitly enabled. The vips processor has been the default since Rails 7.0, meaning the vast majority of modern Rails apps running Active Storage are in scope.
GitHub (as CNA) scored this 9.5 on CVSS v4.0. That score is justified. This is a zero-click, unauthenticated, network-accessible chain that ends in full RCE on the web server, requires only that the app accept image uploads (the entire purpose of Active Storage), and runs in the default configuration. The Ethiack team estimates 500,000+ sites are potentially exposed because the vips processor is default in official Rails Docker images and Debian/Ubuntu packages. The only friction is that the app must actually generate variants from uploads — but thumbnail/avatar generation is the canonical Active Storage use case. noisgate independently assesses this as CRITICAL at 9.3.
5 steps from start to impact.
Identify upload endpoint
- Target runs Rails 7.0+ or 8.x with Active Storage (default config)
- Application exposes at least one image upload endpoint to untrusted users
- Applications that use CarrierWave, Shrine, or Paperclip instead of Active Storage are not affected
- Internal-only apps behind VPN reduce the reachable population
Upload crafted file to trigger unfuzzed loader
rails_vips_oast_poc.py (Ethiack PoC).- libvips built with vulnerable third-party loaders (default on Debian/Ubuntu packages)
- Application generates variants from uploads (thumbnails, avatars, previews)
- Custom libvips builds that exclude MATLAB/HDF5 loaders are not vulnerable, but this is rare
- Variant generation might be async (Sidekiq/GoodJob), adding a small delay but not blocking exploitation
Vips::Error exceptions during variant processing may indicate exploitation attempts.Read SECRET_KEY_BASE and master key
/proc/self/environ (for SECRET_KEY_BASE), config/master.key, or config/credentials.yml.enc. The file contents are embedded in the generated variant image output, which the attacker retrieves via the variant URL. This step yields the cryptographic material needed for step 4.- Rails process has filesystem read access to secret files (always true)
- Variant output is accessible to the requester (standard Active Storage behavior)
- Containerized deployments may limit readable paths, but
/proc/self/environis almost always available - Secret management via external vaults (e.g., AWS Secrets Manager injected at runtime) still exposes env vars
config/master.key reads. Anomalous reads of /proc/self/environ by the Rails worker process. SIEM correlation of variant generation + unusual file access patterns.Forge Marshal deserialization payload for RCE
SECRET_KEY_BASE or the Rails master key in hand, the attacker crafts a signed Ruby Marshal serialization payload. Rails trusts signed cookies and session data deserialized via Marshal.load. The attacker generates a malicious serialized object (e.g., using ysoserial-style Ruby gadget chains) and signs it with the stolen key. Submitting this payload as a session cookie or signed parameter triggers arbitrary code execution as the Rails worker process.- Stolen
SECRET_KEY_BASEor master key from step 3 - Target uses cookie-based sessions or any
Marshal-backed signed data (Rails default)
- Applications using JWT-only sessions or database-backed sessions without Marshal may limit this vector, but cookie sessions are the Rails default
- Modern Rails versions have some Marshal restrictions, but known gadget chains still work
sh, curl, wget).Post-exploitation: lateral movement and data exfiltration
- Successful RCE from step 4
- Network segmentation and cloud IAM least-privilege reduce blast radius
- Container runtimes with read-only filesystems and no-new-privileges limit persistence options
The supporting signals.
| In-the-Wild Status | No confirmed exploitation as of 2026-07-31. Rails Security Team states "no awareness of exploitation or attempted exploitation." Ethiack withheld full technical details until August 28, 2026 to allow patching. |
|---|---|
| Proof-of-Concept | Available. Ethiack published rails_vips_oast_poc.py with a self-contained lab demonstrating the full file-read-to-RCE chain. A third-party GitHub repo appeared within hours of disclosure (July 29, 17:30 UTC). Weaponization is straightforward. |
| EPSS Score | Not yet scored by FIRST (disclosed <48 hours ago). Given the pre-auth/default-config/RCE chain and public PoC, expect high percentile once scored. |
| KEV Status | Not listed as of 2026-07-31. No confirmed in-the-wild exploitation yet, but the 48-hour window since disclosure is short. |
| CVSS Vector (v4.0, GitHub CNA) | CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:H/SI:H/SA:H — 9.5 Critical. Network-accessible, low complexity, no privileges, no user interaction, high impact across all CIA dimensions including subsequent systems. |
| Affected Versions | activestorage < 7.2.3.2, 8.0.0–8.0.5.0, 8.1.0–8.1.3.0. Rails 6.x affected only if config.active_storage.variant_processor = :vips was set manually. Rails 7.0–7.1 and 6.x lines have no upstream fix (EOL). |
| Fixed Versions | 7.2.3.2, 8.0.5.1, 8.1.3.1. Requires libvips ≥ 8.13 and ruby-vips ≥ 2.2.1. The fix calls Vips.block_untrusted(true) at boot. |
| Exposure Data | Ethiack estimates 500,000+ sites potentially exposed. The vips processor is default in official Rails Docker images and Debian/Ubuntu libvips packages. Shodan/Censys fingerprinting of Rails apps is imprecise, but the framework powers a significant share of internet-facing web applications. |
| Disclosure Timeline | 2026-07-22: Ethiack reports to Rails. 2026-07-29: Public advisory, patches released. 2026-08-28: Full technical writeup scheduled. Turnaround: 7 days from report to patch — aggressive. |
| Researchers | Ethiack: André Baptista, Bruno Mendes, Rafael Castilho. GMO Flatt Security: RyotaK (independent discovery). Named KindaRails2Shell. |
noisgate verdict.
The single most decisive factor is that this is an unauthenticated, pre-auth RCE chain against default-configured, internet-facing Rails applications — the attack requires only an image upload form, which is the canonical use case for the affected component. With 500,000+ estimated exposed sites, a public PoC available within hours, and the vips processor being the default since Rails 7.0, the reachable population and weaponization readiness both support a CRITICAL assessment.
Why this verdict
- Pre-auth, zero-click, network-accessible: The attack requires no authentication, no user interaction, and targets a network-facing upload endpoint. There is no friction from access control — anyone who can reach the upload form can exploit this.
- Default configuration is vulnerable: Rails 7.0+ ships with
variant_processor = :vipsas the default. Debian/Ubuntu libvips packages include the vulnerable loaders. The attacker does not need a misconfiguration — they need the *default* configuration. - Full RCE chain with public PoC: The file read alone would be HIGH; the chain to
SECRET_KEY_BASEextraction → signed Marshal deserialization → RCE elevates this to CRITICAL. The PoC (rails_vips_oast_poc.py) was published within hours of disclosure. - Massive reachable population: Ethiack estimates 500,000+ potentially vulnerable sites. Rails is one of the most widely deployed web frameworks, and Active Storage with image uploads is a bread-and-butter feature.
- Role multiplier: Rails is the *typical* web application tier (line-of-business apps, SaaS products, e-commerce). Some high-value deployments include GitLab (CI/CD + supply chain), Discourse (community platforms), Shopify storefronts, and internal admin tools. While GitLab uses CarrierWave (not Active Storage), many internal Rails tools and SaaS products *do* use Active Storage for file handling. On a typical Rails web server, the blast radius is full server compromise + database access + cloud credential theft. For internal admin tools, the chain can pivot to internal network access. The affected component is a web application framework, where ≥80% of installs are internet-facing application servers — this is a canonical high-value-role component for web-tier attacks. Verdict floor: CRITICAL.
Why not higher?
A 9.3 rather than 9.5+ reflects two minor mitigants: (1) the chain requires the application to actually *generate variants* from uploads, not just store them — apps using has_one_attached without ever calling .variant() are not exploitable, and (2) the RCE step via Marshal deserialization has some deployment-specific variability (apps using non-cookie session stores or with custom Marshal restrictions face a slightly higher bar). These narrow the universality slightly below a perfect 10.
Why not lower?
Downgrading below CRITICAL would require evidence that the reachable population is small or that meaningful friction exists in the chain. Neither is true: the vips processor is the *default*, image uploads with thumbnailing is the *canonical* Active Storage use case, the attack is *unauthenticated*, and a *public PoC* exists. The only prerequisite — that the app accepts and processes image uploads — is satisfied by the overwhelming majority of Active Storage deployments. No modern security tool (WAF, EDR) reliably stops this chain at the upload stage because the malicious file is structurally valid.
What to do — in priority order.
- Set VIPS_BLOCK_UNTRUSTED=true environment variable immediately — This tells libvips ≥ 8.13 to refuse loading via unfuzzed/untrusted format handlers, neutralizing the file-read vector entirely. Deploy within 3 days per the noisgate mitigation SLA for CRITICAL. Verify libvips ≥ 8.13 is installed first — older versions ignore this variable silently.
- Add Vips.block_untrusted(true) to a Rails initializer — Alternative to the env var approach. Create
config/initializers/vips_security.rbwithrequire 'vips'; Vips.block_untrusted(true). Requires ruby-vips ≥ 2.2.1. This is the code-level equivalent and survives env var misconfiguration. - Rotate all application secrets after patching — Even if you patch before exploitation, assume secrets *may* have been read. Rotate
SECRET_KEY_BASE,RAILS_MASTER_KEY, database credentials, S3/GCS/Azure storage keys, and all third-party API tokens. ChangingSECRET_KEY_BASEinvalidates all active sessions — plan for user re-authentication. - Disable variant generation for untrusted uploads as a stopgap — If patching or setting the env var is not immediately possible, remove
.variant()calls on user-uploaded attachments. This breaks thumbnails/previews but eliminates the trigger. Only viable as a <3 day bridge. - Upgrade libvips to ≥ 8.13 — The patched Rails versions require libvips ≥ 8.13 to call
block_untrusted. On Debian/Ubuntu, checkvips --version. If running older libvips, the Rails patch will raise a boot exception. Upgrade the system package or use a custom build.
- WAF upload scanning — the malicious file uses a valid image extension and can pass basic MIME-type checks. WAF rules based on file extension or Content-Type will not catch the crafted payload because the exploit leverages legitimate libvips format parsing, not a malformed upload.
- Rate limiting uploads — this is a single-request exploit. Rate limiting does not help; the attacker needs exactly one upload to read a file and one more to deliver the RCE payload.
- Network-level IDS/IPS — the upload and response are standard HTTP/HTTPS traffic. There is no network signature that distinguishes the malicious upload from a legitimate image upload without deep content inspection of the generated variant output.
- Disabling Active Storage entirely — while effective, this is not a compensating control; it removes application functionality. Use
VIPS_BLOCK_UNTRUSTED=trueto surgically disable the vulnerable code path without breaking uploads.
Crowdsourced verification payload.
Run this script on each Rails application host (or in each container) to check the Active Storage version and libvips configuration. Execute as the deploy user or any user with read access to the Gemfile.lock: bash check_cve_2026_66066.sh /path/to/rails/app. No special privileges required.
#!/usr/bin/env bash
# check_cve_2026_66066.sh — Detect CVE-2026-66066 exposure
# Usage: bash check_cve_2026_66066.sh /path/to/rails/app
# Exit codes: 1=VULNERABLE, 0=PATCHED, 2=UNKNOWN
set -euo pipefail
APP_DIR="${1:-}"
if [ -z "$APP_DIR" ] || [ ! -d "$APP_DIR" ]; then
echo "Usage: $0 /path/to/rails/app"
exit 2
fi
GEMFILE_LOCK="$APP_DIR/Gemfile.lock"
if [ ! -f "$GEMFILE_LOCK" ]; then
echo "UNKNOWN — Gemfile.lock not found at $GEMFILE_LOCK"
exit 2
fi
# Extract activestorage version
AS_VERSION=$(grep -A1 '^ *activestorage ' "$GEMFILE_LOCK" | grep -oP '\(\K[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?' | head -1)
if [ -z "$AS_VERSION" ]; then
echo "UNKNOWN — activestorage gem not found in Gemfile.lock (may not use Active Storage)"
exit 2
fi
echo "Detected activestorage version: $AS_VERSION"
# Version comparison helper
version_gte() {
printf '%s\n%s' "$2" "$1" | sort -V -C
}
VULNERABLE=false
# Check version ranges
if version_gte "$AS_VERSION" "8.1.0"; then
if ! version_gte "$AS_VERSION" "8.1.3.1"; then
VULNERABLE=true
FIX="8.1.3.1"
fi
elif version_gte "$AS_VERSION" "8.0.0"; then
if ! version_gte "$AS_VERSION" "8.0.5.1"; then
VULNERABLE=true
FIX="8.0.5.1"
fi
elif version_gte "$AS_VERSION" "7.2.0"; then
if ! version_gte "$AS_VERSION" "7.2.3.2"; then
VULNERABLE=true
FIX="7.2.3.2"
fi
elif version_gte "$AS_VERSION" "7.0.0"; then
VULNERABLE=true
FIX="7.2.3.2 (requires Rails upgrade — 7.0/7.1 are EOL)"
elif version_gte "$AS_VERSION" "6.0.0"; then
echo "NOTE — Rails 6.x only vulnerable if variant_processor was manually set to :vips"
VULNERABLE=true
FIX="7.2.3.2 (requires Rails upgrade — 6.x is EOL)"
fi
# Check for VIPS_BLOCK_UNTRUSTED mitigation
MITIGATED=false
if [ "${VIPS_BLOCK_UNTRUSTED:-}" = "true" ]; then
MITIGATED=true
echo "MITIGATION DETECTED: VIPS_BLOCK_UNTRUSTED=true is set in environment"
fi
# Check libvips version
if command -v vips &>/dev/null; then
VIPS_VER=$(vips --version | grep -oP '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
echo "Detected libvips version: $VIPS_VER"
if ! version_gte "$VIPS_VER" "8.13.0"; then
echo "WARNING: libvips < 8.13 — VIPS_BLOCK_UNTRUSTED env var will be IGNORED"
MITIGATED=false
fi
else
echo "WARNING: vips CLI not found — cannot verify libvips version"
fi
# Final verdict
if [ "$VULNERABLE" = true ]; then
if [ "$MITIGATED" = true ]; then
echo "PATCHED (via mitigation) — activestorage $AS_VERSION is in the vulnerable range but VIPS_BLOCK_UNTRUSTED=true is active with libvips >= 8.13"
exit 0
else
echo "VULNERABLE — activestorage $AS_VERSION is affected by CVE-2026-66066. Upgrade to $FIX or set VIPS_BLOCK_UNTRUSTED=true (requires libvips >= 8.13)"
exit 1
fi
else
echo "PATCHED — activestorage $AS_VERSION is not in the vulnerable range"
exit 0
fiIf you remember one thing.
Gemfile.lock, and deploy the VIPS_BLOCK_UNTRUSTED=true environment variable as an immediate compensating control (noisgate mitigation SLA: ≤ 3 days for CRITICAL). Then schedule the actual gem upgrade to activestorage 7.2.3.2 / 8.0.5.1 / 8.1.3.1 within the noisgate remediation SLA of ≤ 90 days, though given the public PoC and trivial exploitation, pushing this into your next deployment cycle — ideally within 1–2 weeks — is strongly advised. After patching, rotate all secrets: SECRET_KEY_BASE, RAILS_MASTER_KEY, database credentials, S3/GCS/Azure keys, and API tokens. If you're running Rails 7.0, 7.1, or 6.x, there is no upstream fix — you must upgrade your Rails version or apply the VIPS_BLOCK_UNTRUSTED=true workaround and plan a framework migration. Do not wait for KEV listing or confirmed exploitation; the PoC is public and weaponization is trivial.Sources
What defenders are saying.
Crowdsourced verification outputs.
Results submitted by users who ran the verification payload against their environment.