Rails CVE-2026-66066: Patch Isn't Enough

You bumped Rails to 8.1.3.1 on a stock Ubuntu 22.04 image and deployed. The app refused to boot.
That’s the patch working as designed. CVE-2026-66066 - the Active Storage bug the researchers at Ethiack named “KindaRails2Shell” - can’t be fixed by the gem alone. The patched releases check your libvips version at boot and raise when the library is too old. The gem bump is half the job. Getting the container’s libvips to 8.13 and rotating every secret the process could’ve leaked is the rest.
Affected: Active Storage < 7.2.3.2 · >=8.0 <8.0.5.1 · >=8.1 <8.1.3.1, with variant processing
Fixed in: 7.2.3.2 · 8.0.5.1 · 8.1.3.1 (7.1 and older: no patch)
Also required: libvips >= 8.13
CVE-2026-66066 · CVSSv4 9.5 · GHSA-xr9x-r78c-5hrm
Ethiack and GMO Flatt Security reported it, and the Rails security team disclosed it 2026-07-29 in GHSA-xr9x-r78c-5hrm.
Are you exposed? #
The advisory sets three conditions, and together they are the whole test:
- Active Storage uses the libvips variant processor.
config.active_storage.variant_processor = :vipshas been the default sinceload_defaults 7.0, so a 7.x or 8.x app meets this condition without a line of config. - The app accepts image uploads from untrusted users.
- The environment lacks protection against unfuzzed libvips operations: libvips older than 8.13, or 8.13+ with the untrusted-operation block left off.
The advisory closes one door explicitly: “Generating variants is not a separate requirement.” Grepping the codebase for .variant( calls and finding none does not clear you. If the three conditions hold, you’re exposed.
Two of the three are checkable by command. Run these inside the running app; laptop versions prove nothing.
For the first condition and the gem itself, the versions actually loaded:
bin/rails runner 'puts Rails.version'
bundle list | grep -E 'activestorage|ruby-vips|image_processing'
For the third, the libvips your Ruby process links, which is the version that decides everything - the exploit path runs through the library ruby-vips loads in-process:
bin/rails runner 'require "vips"; puts Vips::LIBRARY_VERSION'
# 8.12.x -> vulnerable today, and the patched gem will refuse to boot here
# 8.13.0+ -> the patched gem's guard works
vips --version is a reasonable secondary check where the CLI exists, but slim runtime images often ship libvips42 without vips-tools, so a missing binary tells you nothing about the library your app loaded.
The second condition - untrusted image uploads - is a judgment call about your app, not a grep. A call-site scan still helps you order the work:
rg -n 'has_one_attached|has_many_attached' app/models/
rg -n '\.variant\(|\.representation\(|\.processed\b' app/
Treat those hits as a priority map - where uploads enter, which paths generate images today - not as the exposure gate. An unauthenticated avatar cropper goes to the top of the remediation list; an empty second search changes nothing about whether you patch.
The remediation runbook #
Work top to bottom. The patched gem raises at boot when it finds libvips older than 8.13, so a gem bump that reaches production ahead of the library upgrade takes the app down. Land the libvips upgrade in the same image build as the gem bump, or in the build before it.
1. Get libvips to 8.13 or newer in the image that runs in production.
This is where base images bite. apt-get install libvips on Ubuntu 22.04 (Jammy) installs libvips 8.12.1 - one minor version short, and short in exactly the release that first shipped the untrusted-loader block. Debian and Ubuntu bases below 8.13 need a newer source than the default apt repo, and the Dockerfile should prove it got one:
# Fails the build unless the installed libvips is >= 8.13
RUN dpkg --compare-versions "$(vips --version | cut -d- -f2)" ge 8.13
That gate needs vips-tools in the build stage, and it only proves what the image installed - the Vips::LIBRARY_VERSION check above is what confirms the running process picked the new library up. The Rails 8 Docker production guide covers pinning a library version so the next base rebuild doesn’t quietly regress you to 8.12.
2. Patch the gem - same image build as step one, or after it.
# Gemfile - pick your line
gem "rails", "7.2.3.2" # or 8.0.5.1, or 8.1.3.1
bundle update rails
If you can’t bump the gem yet but the image already carries libvips >= 8.13, the advisory’s workaround is to turn the block on yourself: export VIPS_BLOCK_UNTRUSTED=1, or call Vips.block_untrusted(true) from an initializer with ruby-vips >= 2.2.1. Below libvips 8.13 the advisory is blunt: no workaround exists short of removing the libvips dependency from the app.
3. Rotate every secret the app process can read. This isn’t optional if the app was reachable before you patched.
Akamai spelled out the chain: with secret_key_base in hand, an attacker forges session cookies and signs Global IDs, then hands Rails serialized payloads it will trust. That’s how a file read can become code execution. The advisory hedges the RCE - an attacker “may be able to” invoke an unfuzzed operation - but the file read and the secret exposure behind it are the dependable part of the chain. And the read may have already happened - there are no reliable indicators of compromise for it, so you assume it did.
Rotate, in this order:
RAILS_MASTER_KEY- the read targets/proc/self/environ, which is exactly where it lives. Rotate the key and re-encryptconfig/credentials.yml.enc, or an attacker holding the old key just decrypts whatever new credentials you put in the file.secret_key_base- Active Storage service credentials (S3, GCS, Azure keys)
- Database credentials
- Any third-party API token the app process holds - Stripe, SendGrid, internal service keys
Rotating secret_key_base invalidates more than live sessions. All outstanding signed data dies with it: signed_id links in password-reset and magic-link emails already sent, signed Active Storage blob and service URLs cached in emails or a CDN, signed and encrypted cookies, signed GlobalIDs. Expect dead links in mail that’s already out, and write the support notice before you rotate.
There were no confirmed reports of in-the-wild exploitation at disclosure; a full exploit chain went public around 2026-08-03.
What the bug actually does #
Active Storage generates image variants through libvips by default. config.active_storage.variant_processor = :vips has been the default since load_defaults 7.0, and no later default changed it, so a 7.x or 8.x app runs it whether the config file mentions it or not. Our Active Storage variants walkthrough shows what that processing looks like under normal use.
libvips ships loaders its own maintainers mark “unfuzzed” - safe for images you produced, not for bytes a stranger uploaded. Active Storage handed uploads to those loaders in-process: libvips is linked into your Ruby process through ruby-vips. An attacker uploads a crafted file, your app calls .variant(resize_to_limit: ...) on it, and one of those loaders reads a file off disk that has nothing to do with images.
The file it reads can be /proc/self/environ - the process environment, which is why the rotation list starts with RAILS_MASTER_KEY. Nothing in that chain checks whether the uploader ever logged in.
Why the patched gem fails closed #
The patched gems (7.2.3.2, 8.0.5.1, 8.1.3.1) tell libvips to block untrusted operations. libvips had no way to disable the unfuzzed loaders until 8.13, so on anything older the guard has nothing to switch. Rails handles that case by refusing to run: the advisory states that “Active Storage will raise an exception during boot in such an unsecurable environment.”
That failure mode is a feature. You find out about the version gap at deploy time, from a crash loop in your own output. It’s also the reason the runbook puts the library ahead of the gem - bundle update rails against an 8.12 base image ships a boot crash.
Rails 7.1 and older: no patch is coming #
The security team backported to 7.2, 8.0, and 8.1. Rails 7.1 got nothing; it had finished its security support period by October 2025, and 7.0 and earlier were already out. Same stagger we hit with CVE-2026-41316 three months ago - EOL branches are skipped by policy.
If you’re on 7.1 or below, you have three moves:
- Upgrade to 7.2 or 8.x. A single-major jump is a project with real calendar cost, and the thinner your test coverage, the longer it runs. This CVE is the business case that gets it prioritized.
- Buy backports from a maintained-EOL vendor like HeroDevs if the upgrade can’t land this quarter.
- Break the chain now. The step-2 workaround applies here too once libvips is >= 8.13, or switch the processor off vips:
config.active_storage.variant_processor = :mini_magick. MiniMagick shells out to ImageMagick, which carries its own history of upload CVEs, so treat it as a stopgap while the real upgrade lands.
When this doesn’t apply to you #
Not every Rails app is in scope, and pretending otherwise wastes a maintenance window.
Apps that configured variant_processor = :mini_magick before this disclosure never hand uploads to libvips, so the vulnerable path is off. Apps where every image upload sits behind authentication and admin-only forms carry a narrower risk: the only person who can trigger the read is a logged-in insider. Still worth patching, less worth paging someone at 2am.
Apps that already pin libvips >= 8.13 and run a patched gem are done after the secret rotation. What does not take you out of scope: never calling .variant in your own code - the advisory is explicit that generating variants is not a separate requirement. The one group that can’t opt out: any app taking image uploads from logged-out users on the :vips default.
Where to go next #
Rebuild the production image with libvips >= 8.13 and the patched gem together, confirm the version gate passes inside the container, then rotate. If you deploy with Kamal, the image swap and the secret rotation ride the same release - the multi-server Kamal guide covers pushing a rebuilt base across a fleet.
After that, wire the rubyonrails-security list into a channel your on-call reads, and put a deploy-log alert on failed variant processing so a future crafted upload shows up as signal - Rails 8.1 structured logging is the second half of that setup.
Sources #
- GHSA-xr9x-r78c-5hrm - official Rails advisory
- Rails security discussion: CVE-2026-66066
- Akamai: Defending against KindaRails2Shell
- Rapid7: KindaRails2Shell analysis
- BleepingComputer: Rails patches critical Active Storage flaw
- The Hacker News: Critical Rails flaw
- HeroDevs: CVE-2026-66066 arbitrary file read and RCE