LaunchKit
← All posts
· 10 min read · by The LaunchKit team · 3 views

Active Storage CVE-2026-66066: are you affected, and what to run

CVE-2026-66066 carries a CVSS 4.0 base score of 9.5, needs no login, and the configuration it exposes is the one load_defaults 7.0 hands you. An application running activestorage below 7.2.3.2, 8.0.5.1 or 8.1.3.1 that accepts image uploads and processes them with libvips can have arbitrary files read off the server by an uploaded file, including the process environment holding secret_key_base.

The upgrade is a version bump. Deciding whether you needed it, and what you owe afterwards, is the part that takes an afternoon. This page is the defensive half only.

Decide in one minute whether you are affected

Four conditions have to hold together, and the third one is where most people guess wrong.

bundle list | grep -E "activestorage|ruby-vips|image_processing"
bin/rails runner 'puts ActiveStorage.variant_processor'
vips --version

The first command gives you your activestorage version and tells you whether ruby-vips is in the bundle at all. The second prints vips or mini_magick: ActiveStorage.variant_processor is set in the engine's after_initialize from config.active_storage.variant_processor, so this is the value your booted application actually uses, not the one you remember writing. The third matters for the workaround, not for the exposure.

You are in scope when all four are true: activestorage is below the patched version for your line, ruby-vips is installed, the variant processor is :vips, and untrusted users can get a file into an Active Storage attachment. On the fourth, "untrusted" means anyone who is not you. A logged-in customer uploading an avatar is untrusted for this purpose, because the advisory's impact does not depend on who the attacker is, only on a file reaching libvips.

What is not a check: whether the attachment is displayed anywhere, whether the upload form has a accept="image/*" attribute, and whether your validations reject non-images by content type. CVE-2026-33173 below exists precisely because content type is a claim, not a fact.

The three patched releases

Rails shipped 7.2.3.2, 8.0.5.1 and 8.1.3.1 on 29 July 2026, and the advisory lists the affected ranges as activestorage < 7.2.3.2, >= 8.0.0.beta1, < 8.0.5.1, and >= 8.1.0.beta1, < 8.1.3.1. Bumping the rails gem is enough, since activestorage ships in lockstep with it.

Nothing shipped on 7.1, 7.0 or 6.x. The affected range for those lines reads < 7.2.3.2, which is not a typo: the release announcement says "Older versions of Rails are unsupported, and users are recommended to upgrade to at least the 7.2 series." State that cost honestly to whoever is asking you for an ETA. Going from 7.1 to 7.2.3.2 is a framework upgrade with a deprecation pass in it, not a patch bump you merge on a Friday, and the interim between now and that merge is the window the next section is about.

When you cannot upgrade today

Three mitigations reach the same end state as the patch, and all three need libvips 8.13 or later.

  • Set the VIPS_BLOCK_UNTRUSTED environment variable in the process running your app and jobs.
  • Call Vips.block_untrusted(true) from an initializer, which needs ruby-vips 2.2.1 or later.
  • Remove ruby-vips from the Gemfile entirely. The advisory calls this out for applications that declared it only for image analysis and never generate variants.

On libvips older than 8.13 the advisory is blunt that there is no workaround other than removing the dependency. That version floor is not arbitrary: Vips.block_untrusted is the switch, and 8.13 is the first release that has it.

Then rotate. The advisory's remediation covers secret_key_base, the master key, storage service credentials, database credentials and third-party tokens, because the stated impact is a read of the process environment and anything the process could open. An unexploited vulnerability does not need a rotation; one you cannot prove was unexploited does, and on a public upload endpoint you usually cannot prove it from access logs alone.

A default assigned in the 7.0 branch and never revisited

config.active_storage.variant_processor defaults to :mini_magick in the engine, and then railties overwrites it. In railties 8.1.3.1, lib/rails/application/configuration.rb assigns active_storage.variant_processor = :vips at line 255, inside the when "7.0" branch that starts at line 219 and ends when when "7.1" starts at line 269. No later branch reassigns it. Any application on load_defaults 7.0 or higher processes variants with libvips whether or not anyone on the team ever typed the word vips.

That is why the CWE on this advisory is CWE-1188, "Initialization of a Resource with an Insecure Default", rather than a memory-safety class. Nothing here is a bug in code somebody wrote. It is two correct-looking decisions composing into an exposure: Rails picked a faster image processor as a framework default, and never told that processor to refuse the formats its own maintainers flag as unsafe for untrusted input.

Running Rails 8 without Redis is the same shape from the correctness side rather than the security side. A rate limit counter ends up in PostgreSQL because rate_limit's store: default and config.cache_store agree with each other, and nobody decided that either. Defaults that compose are the ones worth auditing, because no line in your repository records the decision.

What libvips means by unfuzzed

libvips reads a long list of image formats, and delegates some of them to other libraries. The Active Storage CHANGELOG entry for 8.1.3.1 states the distinction directly: libvips "flags some of its loaders and savers as 'unfuzzed' or 'untrusted', meaning they are only safe for trusted content."

The class of bug is worth understanding even if you patch and move on. A file arriving at a format dispatcher decides which parser runs, because that is what format dispatch is for. The set of parsers reachable that way was larger than the set anybody had hardened, and the layer above it, Active Storage, never narrowed the set. Your own validations sat above that, and they were checking a content type the uploader supplied. At no point did a component hold an allowlist of what it was willing to parse, and each layer was reasonable in isolation.

The fix is exactly the narrowing that was missing, and it is one statement. lib/active_storage/vips.rb in the patched gem calls Vips.block_untrusted(true) at load time. The ordering in that file is the interesting part: it requires image_processing/vips first, on purpose, with a comment explaining that letting image_processing load later would disable the loaders again after an application's initializers had deliberately re-enabled one.

Variants are not the only way a file reaches libvips

Grepping for .variant( is the check almost everyone runs first, and it is the wrong turn on this advisory. Analysis reaches libvips on its own.

# activestorage-8.1.3.1/lib/active_storage/analyzer/image_analyzer/vips.rb
def self.accept?(blob)
  super && ActiveStorage.variant_processor == :vips
end

def read_image
  download_blob_to_tempfile do |file|
    image = instrument("vips") do
      ::Vips::Image.new_from_file(file.path, access: :sequential)

ActiveStorage::Attachment calls blob.analyze_later unless blob.analyzed? when a file is attached, and ActiveStorage::Analyzer::ImageAnalyzer::Vips is first in the default config.active_storage.analyzers array. So attaching an image is enough to open it with a libvips loader, in a background job, with no variant requested anywhere in your codebase. The CHANGELOG confirms the analyzer was running those loaders, because it lists what stops working after the patch: "analysis of these and other types such as SVG, JPEG XL, JPEG 2000, and Netpbm will no longer record width and height."

Your test suite will not catch this either. A request spec that attaches a fixture and asserts a 302 stays green whether the analyze job ran, raised, or was never enqueued, and in most test setups the job never runs at all.

The Rails boilerplate this site sells is the case that looks safe and still has to be checked twice: its Gemfile.lock pins rails (8.1.3.1) on the patched line, and its only Active Storage declaration is has_many_attached :attachments on Message, with no .variant call anywhere in the tree. The version pin is what mattered, since the absence of .variant would not have been sufficient on its own.

What the patch breaks

Taking this upgrade costs you image formats, and the advisory says so rather than hiding it. After 7.2.3.2, 8.0.5.1 or 8.1.3.1, variant transformation of BMP, ICO and PSD attachments raises Vips::Error. Analysis of those, plus SVG, JPEG XL, JPEG 2000 and Netpbm, stops recording width and height. Requesting an unfuzzed output format, typically FITS or JXL, also raises. Attaching, storing and downloading are unchanged.

Where that failure surfaces depends on when you transform. An application generating variants in a background job gets failed jobs it can see in a dashboard. An application transforming during a request turns the same exception into a 500 for a user, which is the case Rails singles out and the one to check first. The documented escape is to stop treating those types as variable at all.

Rails.application.config.active_storage.variable_content_types -=
  %w[ image/bmp image/vnd.microsoft.icon image/vnd.adobe.photoshop ]

There is a second breaking change that has nothing to do with formats. The patched gem raises at boot when it cannot disable the loaders, with a message worth recognising before you meet it at three in the morning: "libvips's unfuzzed operations are not safe to use with untrusted content, and Active Storage cannot disable them." The minimum is now libvips 8.13 and ruby-vips 2.2.1, and a container image pinned to an older base can absolutely trip this on deploy rather than in CI.

Applications on the :mini_magick processor are not off the hook for the boot check. Their attachments process unchanged, but the CHANGELOG notes the loaders are disabled process-wide whenever ruby-vips is installed, and the version minimums still apply. Removing ruby-vips from the Gemfile avoids both.

Take the breakage anyway. Losing PSD thumbnails is a support ticket, and the alternative is an unauthenticated read of secret_key_base.

The four March advisories

Active Storage had a worse year than usual. Rails shipped 7.2.3.1, 8.0.4.1 and 8.1.2.1 on 23 March 2026, and four of the CVEs in that batch were Active Storage. If you are upgrading for 66066 now, you clear all four on the way.

  • CVE-2026-33202, glob injection in DiskService. DiskService#delete_prefixed passed blob keys straight to Dir.glob without escaping glob metacharacters, so a key carrying *, ?, [ or {} could match and delete files beyond its own prefix. CVSS 4.0 base 6.6, CWE-74. Reachable only if your blob keys carry attacker-controlled input or come from a custom key generator.
  • CVE-2026-33173, content type bypass in direct uploads. DirectUploadsController accepted arbitrary metadata from the client and persisted it on the blob, and the internal identified and analyzed flags live in that same hash. Setting them skips MIME detection, so a validation trusting Active Storage's own identification could be told what to believe. CVSS 4.0 base 5.3.
  • CVE-2026-33174, unbounded Range DoS in proxy mode. Blobs::ProxyController loaded the whole requested byte range into memory before sending it, so Range: bytes=0- on a large blob allocated memory proportional to the file.
  • CVE-2026-33658, multi-range DoS in proxy mode. The same controller put no limit on the number of byte ranges in a Range header, and thousands of small ranges cost disproportionate CPU.

The two DoS entries only apply in proxy delivery mode. An application on redirect mode, which is the default, never runs that controller for blob bytes.

What is not on this page

No reproduction is here, and that is deliberate. There is no crafted file, no payload, no step-by-step chain and no link to a proof of concept, including ones that are easy to find. The class of bug is genuinely useful to understand and a working chain is not something this site hands out.

Also not covered: how to tell whether you were already hit, which is a forensics question that depends on your logs and your storage service; the ImageMagick side of variant processing, which has its own history and is not what this advisory is about; and non-image attachments, which the analyzer path does not touch. For the authoritative text, read the advisory on discuss.rubyonrails.org and GHSA-xr9x-r78c-5hrm rather than this page or any other summary.

#rails #security

Comments

No comments yet. Be the first.

Only used to confirm and publish your comment. Never shown publicly, never shared.

Markdown: **bold**, `code`, ```fenced blocks```, > quotes, [links](url). HTML and images are not rendered.