# Verification Report — CVE-2026-66066 (GHSA-xr9x-r78c-5hrm)

## Fix Summary

The patch backports the exact upstream security fix (Rails 8.0.5 → 8.0.5.1,
also shipped as 7.2.3.2 / 8.1.3.1) to the vendored vulnerable
`activestorage-8.0.5` gem: at application boot, Active Storage now loads
ruby-vips and calls **`Vips.block_untrusted(true)`**, a process-wide libvips
switch that makes every libvips operation flagged `VIPS_OPERATION_UNTRUSTED`
("unfuzzed") — including `matload` (matio/HDF5, the loader used by the
demonstrated arbitrary-file-read exploit), `svgload`, `pdfload`,
`magickload`, etc. — refuse to run against attacker-controlled uploads. The
fix also **fails closed**: boot raises a RuntimeError when the stack cannot
be secured (libvips < 8.13 or ruby-vips < 2.2.1), instead of silently
processing untrusted content.

## Changes Made

`bundle/coding/proposed_fix.diff` (paths relative to the activestorage gem
root, apply with `patch -p1`):

1. **`lib/active_storage/vips.rb` (new file)** — byte-identical to upstream
   8.0.5.1. Requires `nokogiri` first if present (libxml2 symbol-ordering
   workaround), then ruby-vips; sets `ActiveStorage::VIPS_AVAILABLE` (false
   when ruby-vips is absent — then the `:vips` processor cannot be used and
   no CVE surface exists); requires `image_processing/vips` *before*
   blocking (image_processing 2.x blocks untrusted ops itself at load, and
   loading it later would re-disable loaders an app's initializers had
   re-enabled); raises at boot on an unsecurable stack; finally calls
   `Vips.block_untrusted(true)`.
2. **`lib/active_storage/analyzer/image_analyzer/vips.rb`** — adds
   `require "active_storage/vips"` at the top. `active_storage/engine.rb`
   already eagerly requires this analyzer at engine load, so the hardening
   executes at boot before any request is served.

The upstream `PRE = "1"` version bump and CHANGELOG entry are intentionally
omitted from the minimal security patch.

## Verification Steps

`bundle/coding/verify_fix.sh` (idempotent; full log in
`bundle/coding/verify_fix.log`):

1. Applies `proposed_fix.diff` with `patch -p1` to a pristine copy of the
   vendored `activestorage-8.0.5` — applies cleanly (dry-run + real).
2. Static assertions: `Vips.block_untrusted(true)` present in the new
   `vips.rb`; analyzer requires `active_storage/vips`.
3. Equivalence: both patched files are **byte-identical** to the vendored
   upstream fixed release `activestorage-8.0.5.1` (whole-`lib/` diff shows
   only `gem_version.rb` differs, the omitted version bump).
4. Dynamic assertions in fresh ruby processes (ruby 3.3.8, system libvips
   8.18.0 with `matload` flagged `untrusted`, ruby-vips 2.3.0,
   image_processing 1.14.0 — matching the repro build), loading
   Active Storage exactly as `active_storage/engine.rb` does at boot, then
   attempting `Vips::Image.new_from_file` on the actual CVE payload
   (`bundle/vuln_variant/work/payloads/evil.mat`, MATLAB v7.3/HDF5 with
   external-storage segments pointing at `/proc/self/environ`):

```
=== patched (expect blocked) ===
matload of crafted payload: error: VipsForeignLoad: ".../evil.mat" is not a known file format
CHECK PASS (blocked)
=== unpatched (expect unblocked) ===
matload of crafted payload: loaded 99x1
CHECK PASS (unblocked)
RESULT: FIX VERIFIED
```

Note on the error string: libvips ≥ 8.17 excludes blocked loaders from the
loader search entirely, so the refusal surfaces as "not a known file
format"; libvips 8.13–8.16 (e.g. the repro's 8.14.1) raises
"matload: operation is blocked". Both are fail-closed refusals; the verify
script accepts either. The runtime repro already proved end-to-end that the
8.0.5.1 build (same code this patch produces) serves no variant and leaks
nothing (`fixed blocked=2/2`).

## Test Results

- Patch application: clean (also re-runnable; verify script exits 0 on
  consecutive runs — idempotent).
- Static + equivalence checks: PASS (identical to upstream 8.0.5.1).
- Patched gem: crafted payload REFUSED — PASS.
- Unpatched control: crafted payload LOADS as a 99x1 image — PASS,
  confirming the divergence is caused by the patch and not the environment.
- Edge cases covered by the fix code itself (inherited from upstream):
  ruby-vips absent from the bundle → nothing blocked, nothing needed;
  libvips < 8.13 / ruby-vips < 2.2.1 → loud boot-time failure instead of
  insecure operation; image_processing 2.x load-ordering handled.

## Remaining Concerns

- **Out of scope (separate issue):** the `:vips` transformer still performs
  no transformation validation (upstream rails issue #56948); a forged
  variation token can inject arbitrary keys such as `instance_eval`. That
  second stage requires `secret_key_base`, which this CVE's file read was
  the means to obtain — closing the file read breaks the demonstrated RCE
  chain, but transformation validation should be tracked upstream.
- `block_untrusted` is process-global; an operator may re-enable specific
  loaders in an initializer (documented upstream breaking-change note).
  That is a deliberate configuration choice, not an attacker-reachable
  bypass.
- On libvips < 8.13 there is no secure workaround other than removing the
  ruby-vips dependency; the fix correctly refuses to boot in that case.
- Operational: after deploying, rotate `secret_key_base` and any credentials
  present in the application environment, per the advisory.
