Why Patching the Gem Didn't Fix CVE-2026-66066

Why Patching the Gem Didn't Fix CVE-2026-66066

You saw the advisory for CVE-2026-66066 opens a new window , the Active Storage vulnerability in the way Rails processes image variants with libvips. You bumped activestorage to the patched version, ran your tests, deployed, and moved on. That is the responsible thing to do, and for most Ruby vulnerabilities it would be the whole job.

This one is different. The patched version of Active Storage will not run on an old copy of libvips. It raises an exception during boot and refuses to start:

/usr/local/bundle/gems/activestorage-8.1.3.1/lib/active_storage/vips.rb:36:in '<compiled>': libvips's unfuzzed operations are not safe to use with untrusted content, and Active Storage cannot disable them. Disabling them requires libvips 8.13 or later and ruby-vips 2.2.1 or later. Please upgrade libvips and ruby-vips, or remove the ruby-vips gem from your Gemfile. (RuntimeError)

libvips opens a new window is a system library that lives in your container image, not a gem in your Gemfile, so bumping the gem does nothing on its own until you rebuild the image. If your image still ships the old library, you are in one of two states, and neither is the one you think you are in: either your app won’t boot, or it never received the patched gem at all and is quietly still vulnerable.

In this post, you’ll learn why upgrading activestorage doesn’t close CVE-2026-66066 on its own, how a stale container image both hides the vulnerability and blocks the fix, and why rebuilding on the wrong base still leaves you exposed.

The CVE, and why the gem bump isn’t the whole fix

CVE-2026-66066 is a vulnerability in the way Active Storage processes image variants. When Active Storage generates a variant, a thumbnail or a resized copy, it hands the work off to libvips, a fast image-processing library. The trouble is that some of libvips’s operations are not safe to run on untrusted input, and older versions of the library give you no way to turn those operations off. An attacker who can get your app to process a malicious file can abuse this. The official Rails advisory opens a new window describes the impact as unauthenticated arbitrary file read that can escalate to remote code execution. Its severity rating is Critical, and public write-ups of the exploit are already circulating.

As of early August 2026 it is not yet listed in CISA’s Known Exploited Vulnerabilities catalog opens a new window , but that is not a reason to relax. Arbitrary file read on a production host usually means your credentials and environment variables are readable, which is often enough for an attacker to move further into your infrastructure.

If you use Active Storage with image variants, you are very likely affected. The vips processor has been the default variant processor opens a new window since config.load_defaults 7.0, and it has stayed the default since, so most modern Rails apps rely on it without ever opting in. The versions that carry the fix are activestorage 7.2.3.2, 8.0.5.1, and 8.1.3.1.

Patching the gem is normally the reliable fix for a Rails CVE, and usually that is where the story ends (we have written before about several Rails CVEs worth checking opens a new window ). If your app is on an end-of-life branch like 7.0 or 7.1 opens a new window , there is no patched release to bump to at all, and your only options are upgrading libvips together with the mitigation below, or upgrading Rails to a supported version.

The fix is genuinely both/and: you need the patched activestorage and you need libvips 8.13 opens a new window or newer, because libvips 8.13 is the first version that can block those unsafe operations. Patched Active Storage checks for that capability when it boots, and if libvips is too old it raises the exception I showed above and refuses to start rather than run in what the advisory calls an “unsecurable environment”. The newer library does the actual securing; the gem only knows how to ask for it.

If you can’t bump the gem right away, the advisory opens a new window documents two workarounds that still require libvips 8.13 or newer: calling Vips.block_untrusted(true) from an initializer (with ruby-vips 2.2.1 or newer), or setting the VIPS_BLOCK_UNTRUSTED environment variable, which libvips reads at initialization.

And if there is any chance your app processed a malicious upload before you patched, rotate whatever secrets were reachable from that host. To find out whether that actually happened, Rails published forensic tooling opens a new window that scans your Active Storage blobs for the crafted files the attack depends on and estimates how long you were exposed, though the maintainers note a clean result is strong evidence rather than proof.

The proof: same gem, two images

To make the problem concrete, I built a small demo: one Rails app, one Dockerfile, and two images that differ in exactly one thing. The app has the patched activestorage (8.1.3.1) and Active Storage configured with the vips variant processor. The boot check fires from the configuration and the environment, not from a request.

The Dockerfile takes the Debian suite as a build argument, so the only thing that changes between the two builds is the base image, and therefore the version of libvips that apt installs:

# syntax=docker/dockerfile:1
ARG RUBY_VERSION=3.4
ARG SUITE=bookworm
FROM docker.io/library/ruby:${RUBY_VERSION}-slim-${SUITE}

# libvips comes from the OS package repo, NOT from a gem.
# The base image (SUITE) decides which libvips version you get:
#   bullseye -> 8.10.x (< 8.13, cannot block untrusted ops)
#   bookworm -> 8.14.x (>= 8.13, can)
RUN apt-get update -qq \
 && apt-get install --no-install-recommends -y build-essential git libvips libvips-tools libpq-dev pkg-config \
 && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle install
COPY . .

ENV RAILS_ENV=production \
    SECRET_KEY_BASE_DUMMY=1

# Print the receipts, then boot.
CMD ["bash", "-lc", "echo '== libvips =='; vips --version; echo '== activestorage =='; bundle list | grep activestorage; echo '== boot =='; bin/rails runner 'puts :booted_ok'"]

Then I built the same file twice, changing only the suite:

docker build -f Dockerfile.demo --build-arg SUITE=bookworm -t asdemo:new .
docker build -f Dockerfile.demo --build-arg SUITE=bullseye -t asdemo:old .

Each container prints its libvips version, confirms which activestorage is installed, and then tries to boot the app. Running the bookworm image first:

$ docker run --rm asdemo:new
== libvips ==
vips-8.14.1
== activestorage ==
  * activestorage (8.1.3.1)
== boot ==
booted_ok

libvips 8.14.1, the patched gem, and a clean boot. Now the bullseye image, which is identical except that apt installed an older libvips:

$ docker run --rm asdemo:old
== libvips ==
vips-8.10.5-Wed Apr 30 17:29:54 UTC 2025
== activestorage ==
  * activestorage (8.1.3.1)
== boot ==
/usr/local/bundle/gems/activestorage-8.1.3.1/lib/active_storage/vips.rb:36:in '<compiled>': libvips's unfuzzed operations are not safe to use with untrusted content, and Active Storage cannot disable them. Disabling them requires libvips 8.13 or later and ruby-vips 2.2.1 or later. Please upgrade libvips and ruby-vips, or remove the ruby-vips gem from your Gemfile. (RuntimeError)
        ... backtrace trimmed ...

But the bullseye image ships libvips 8.10.5, which is older than 8.13, so patched Active Storage does exactly what the advisory says it will and refuses to boot. To rule out the gem layer, ruby-vips is 2.3.0 in both images and the Gemfile.lock is identical. The only moving part is the system library. The same patched Gemfile produced one app that boots and one that won’t.

Rebuilding the image isn’t always enough

The obvious fix is to rebuild the image, and that is the right instinct, but it comes with a trap. Rebuilding only helps if the base image you rebuild on actually carries a newer libvips, and Debian bullseye does not. Checking the bullseye base while writing this, apt still installed libvips 8.10.5, the same version the demo hit. bullseye is an older Debian release, and its package repositories, security updates included, are effectively frozen at libvips 8.10 opens a new window , so a rebuild faithfully reinstalls exactly what is there.

The version of libvips you get is really a property of the base image’s distribution, not of how recently you rebuilt. To get a patched version of libvips, you have to move to a base whose repositories carry it. Moving from bullseye to bookworm is the simplest path, since bookworm ships libvips 8.14 opens a new window . If you are pinned to an older distribution for another reason, compiling a newer libvips yourself is the alternative, though moving the base image to bookworm is usually less work and less to maintain afterward.

A rebuild that stays on the same old base is not a fix; it just reproduces the vulnerable environment with a fresh timestamp.

The image that quietly stays vulnerable

There is a worse case than the app that refuses to boot, and it is the quiet one. The image that raises on boot at least tells you something is wrong, because the app is down and you notice right away. The dangerous image is the one that was never touched at all. It still runs the old, unpatched activestorage on the old libvips, it boots perfectly, and it keeps serving traffic while remaining fully exploitable. Nobody gets an exception, and nothing looks broken.

The instinct is to reach for image age to find these, but the age of a tag does not tell you what you need to know. An image tagged last week can still be built on a base that is two years old, and an image built months ago might be sitting on a perfectly current base. What you actually care about is whether the image was rebuilt since the base it depends on last changed. An image that has fallen behind its own base has missed whatever security updates that base shipped, libvips among them, even if its tag looks recent.

This is the same kind of blind spot we write about on the gem side. A tool like bundler-audit opens a new window scans your Gemfile.lock for known-vulnerable gems, but it reads the lockfile, so it has no way of knowing which version of libvips your base image installed.

In our experience auditing client deployments, stale base images are the rule rather than the exception. CVE-2026-66066 is a good reason to check whether your own images have drifted, and to make rebuilding them a habit rather than a one-time scramble.

Conclusion

The fix for CVE-2026-66066 requires two things: the patched gem and a base image that ships libvips 8.13 or newer. Patching the gem without rebuilding the image leaves you in one of two bad states. Image drift is not a problem you fix once because base images drift the same way gems do, and the only thing that keeps them current is a rebuild cadence you actually stick to.

We can help you audit what your deployed images are actually running and find the gaps your Gemfile cannot see. Send us a message! opens a new window

Get the book