GHSA-F3W6-W445-FWH9: Verified Reproduction
GHSA-F3W6-W445-FWH9: Rails Active Storage ImageMagick image analysis: arbitrary file write and read possible RCE
GHSA-F3W6-W445-FWH9 is verified against activestorage (rubygems) · rubygems. Affected versions: activestorage < 8.0.5.2 and >= 8.1, < 8.1.4.1. Fixed in 8.0.5.2, 8.1.4.1. This critical reproduction includes runnable sandbox proof, artifacts, and a plain-text agent view under REPRO-2026-00389.
What Is GHSA-F3W6-W445-FWH9?
GHSA-F3W6-W445-FWH9 is a critical-severity vulnerability affecting activestorage (rubygems) activestorage < 8.0.5.2 and >= 8.1, < 8.1.4.1. Pruva has independently reproduced it and publishes a verified, runnable proof-of-concept (reproduction REPRO-2026-00389).
GHSA-F3W6-W445-FWH9 Severity
GHSA-F3W6-W445-FWH9 is rated critical severity.
Critical — the most severe class — typically remotely exploitable with severe impact. Treat as an emergency.
Affected activestorage (rubygems) Versions
activestorage (rubygems) · rubygems versions activestorage < 8.0.5.2 and >= 8.1, < 8.1.4.1 are affected.
How to Reproduce GHSA-F3W6-W445-FWH9
pruva-verify REPRO-2026-00389 curl -O https://www.pruva.dev/api/v1/reproductions/REPRO-2026-00389/artifacts/bundle/repro/reproduction_steps.sh && chmod +x reproduction_steps.sh && ./reproduction_steps.sh Proof of Reproduction for GHSA-F3W6-W445-FWH9
- reached the target end-to-end
- full exploit chain demonstrated
- on the real production code path
- high confidence
- the upstream fix blocks the same trigger
Unauthenticated SVG upload whose <image xlink:href> carries an explicit msl: coder prefix naming an attacker-uploaded MSL script at a blob path recovered from the signed disk token
- POST /uploads (Active Storage attach)
- analyzer runs at attach time
- MiniMagick identify
- ImageMagick SVG coder
- MSL coder
- <read>/<write> of attacker-named files
- arbitrary file write, exfiltration through the attacker's own blob, code execution via a written template
How the agent worked
Root Cause and Exploit Chain for GHSA-F3W6-W445-FWH9
Active Storage image analysis: arbitrary file write, arbitrary file read, and code execution through ImageMagick's MSL scripting coder reached from an attached upload.
activestorage hands every attached upload to ImageMagick for image analysis without
constraining which of ImageMagick's coders the file may reach. ImageMagick selects a coder
from the content of the file it is given, and some coders act on files named inside the
image rather than on the image itself. A file whose content is XML is sniffed as SVG
(ImageMagick's built-in magic table maps ?xml/?xml/SVG/svg at offset 1 to the SVG
coder), and ImageMagick's own SVG renderer resolves the xlink:href of every <image>
element through ReadImage. Because that reference may carry an explicit coder prefix,
an uploaded SVG containing <image xlink:href="msl:/path/to/file"/> makes ImageMagick parse
the named file with the MSL ("Magick Scripting Language") coder. MSL is a scripting coder
that reads and writes the files its script names, so the upload alone — no variant, preview,
or analyze call — gives an unauthenticated attacker arbitrary file write, and (with the
Disk service, where the attacker's own blob path is knowable) arbitrary file read.
This run reproduced the full chain against the real product code: activestorage at the
parent of the fix commit, driven through a real HTTP upload endpoint, with a real Puma
server, a real Active Storage Disk service, the real ImageMagick analyzer, and a real
ImageMagick process. It also demonstrated escalation to code execution by writing the
attacker's content into a path the application loads and renders.
- Component:
activestorage(Ruby on Rails) —ActiveStorage::Analyzer::ImageAnalyzer::ImageMagickand, more fundamentally, the absence of any ImageMagick coder restriction in Active Storage. - Affected versions:
activestorage < 8.0.5.2and>= 8.1, < 8.1.4.1; an application is affected only when all of the following hold:config.active_storage.variant_processor = :mini_magick(default forload_defaults6.1 or earlier; the same setting selects the analyzer),- ImageMagick runs under a policy that does not deny the coders involved (the policies shipped by Debian and Ubuntu do not deny MSL),
- untrusted users can attach uploads.
- Risk level: critical (advisory CVSS 3.0 9.8,
AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H). - Consequences observed in this run: attacker-chosen file written at an attacker-chosen
path with attacker-chosen bytes; files readable by the application process read and
exfiltrated over HTTP (the process environment including
SECRET_KEY_BASE, and a planted credential file); attacker-supplied Ruby executed inside the application process.
Impact Parity
- Disclosed/claimed maximum impact: the advisory asserts arbitrary file write and
arbitrary file read when attachments are stored with the Disk service, and states that
readable secrets "may in turn allow escalation to remote code execution". The submitted
claim contract expects
code_executionon theapi_remotesurface. - Reproduced impact from this run:
- arbitrary file write —
full(exact attacker bytes at an attacker-chosen absolute path), - arbitrary file read —
full(a planted credential file and/proc/self/environincludingSECRET_KEY_BASE, both exfiltrated over HTTP through the attacker's own blob), - code execution — demonstrated (unique marker file content plus
idoutput reflected in an HTTP response).
- arbitrary file write —
- Parity:
full. - Not demonstrated / conditions: the code-execution leg required the write target to be a
path the application loads and re-reads, which the reproduced application has because it runs
with code reloading enabled (Rails development defaults). The advisory-asserted write and read
primitives are unconditional. A deployment with eager loading, template caching, and a
non-writable application tree would still be fully exposed to arbitrary file write and read;
its escalation to code execution would then depend on the deployment (a scheduled job, a
writable autoload path, an executable script,
ld.so.preloadfor a root-run process, and so on) and was not separately validated here.
Root Cause
- Coder selection is attacker-controlled.
ActiveStorage::Analyzer::ImageAnalyzer::ImageMagick#read_imagedownloads the blob and runsMiniMagick::Image.new(file.path)(i.e.magick identifyon the file). Nothing in Active Storage restricts the set of coders ImageMagick may pick, so the uploader decides which parser handles the bytes. ImageMagick's built-in magic table maps XML-looking content to the SVG coder. - The SVG coder resolves references through ImageMagick's read path.
coders/svg.cstores thexlink:hrefof<image>insvg_info->urland passes it toStrictReadImage()/ReadImage()(and emitsimage Over x,y w,h "url"into the MVG it renders). An explicit coder prefix in that reference is honoured, somsl:<path>selects the MSL coder for the named file. - MSL acts on files named inside the script.
coders/msl.cimplements<read>and<write>:readcallsReadImageon any filename (raw coders such asgray:read the file's bytes verbatim) andwritecallsWriteImageto any path. Both run with the privileges of the Rails process. - The Disk service makes the primitive remotely useful. The blob key is recoverable by
decoding the signed (not encrypted) disk-service token that Active Storage puts in the
Locationheader of the blob redirect, so the attacker knows the on-disk path of their own blob. The MSL script can therefore copy any readable file's bytes into that blob file, and the attacker downloads them through the ordinary blob URL — an arbitrary file read over HTTP. Overwriting any other writable path gives the arbitrary file write. - Trigger point.
ActiveStorage::Attachmentrunsafter_create_commit :analyze_blob_later, so analysis — and therefore the MSL execution — happens in the request that attaches the file. No variant, preview, or explicitanalyzecall is needed.
Fix commit: ea8c40121ce53a3d0255cd4eb6d13bfe3bc01776 ("Ship an ImageMagick security policy
for every MiniMagick process", 8.0 branch; 8.1 branch 468aa9a93fc98f5476b6821770f4ccfcb7f7d328).
The fix ships activestorage/lib/active_storage/image_magick/policy.xml and prepends its
directory to MAGICK_CONFIGURE_PATH through MiniMagick.cli_env, so every ImageMagick process
MiniMagick spawns denies MSL, the other file-reaching coders, the network coders, the
Ghostscript coders/delegate, and @filename indirection. It also raises at boot for
:mini_magick applications whose mini_magick is older than 5.1 (which cannot apply the
policy) — a behaviour observed directly in this run when the fixed application was first
started with mini_magick 5.0.1.
Reproduction Steps
bundle/repro/reproduction_steps.shis the single entrypoint. It:- resolves the Rails repository from
bundle/project_cache_context.json(<project_cache_dir>/repo), creates a worktree of the vulnerable commit (ea8c40121ce53a3d0255cd4eb6d13bfe3bc01776^,RAILS_VERSION8.0.5.1) and of the fixed commit (ea8c40121ce53a3d0255cd4eb6d13bfe3bc01776), and asserts the patch is absent from the first and present in the second; - builds
bundle/repro/docker/Dockerfile: ImageMagick 7.1.1-32 built from upstream source without librsvg (so the SVG coder uses ImageMagick's own internal renderer, exactly as in the reported chain) under the Debian/Ubuntu-stylepolicy.xml(which does not deny MSL), plus two minimal but real Rails applications (/app-vuln,/app-fixed) that useactivestoragefrom the two worktrees,variant_processor = :mini_magick, the Disk service,active_job.queue_adapter = :inline,mini_magick 5.0.xfor the vulnerable app andmini_magick 5.1+for the fixed app; - starts both applications under Puma with
SECRET_KEY_BASEin the process environment, plants a credential file at/srv/pruva_credentials.txt, and runsbundle/repro/attack/attack.pytwice against each application.
- resolves the Rails repository from
attack.pyis a pure HTTP client. It fetches the upload form and its CSRF token, attaches four uploads throughPOST /uploads(an exfiltration blob, a second exfiltration blob, the attacker's payload bytes, and the MSL script), recovers each blob key by decoding the signed disk token in the blob redirect, attaches the SVG<image xlink:href="msl:<blob path>">(which triggers the analyzer at attach time), retries the environment read with decreasing sizes, and then verifies the effects.- Expected evidence: on the vulnerable application the attacker's bytes appear at
/tmp/pruva_arbitrary_write.txt, the attacker's own blob now carries the server's process environment (includingSECRET_KEY_BASE) and the planted credential file, and a subsequent page request executes the template the MSL write replaced. On the fixed application nothing happens and the server log shows ImageMagick refusing the coder:attempt to perform an operation not allowed by the security policy 'MSL'.
Evidence
All paths are relative to bundle/.
| Evidence | Location |
|---|---|
| Reproduction log (both runs) | logs/reproduction_steps.log |
| Vulnerable attempt 1 / 2 attack transcript | logs/attempts/vuln_1/attack.stdout, logs/attempts/vuln_2/attack.stdout |
| Structured per-attempt results | logs/attempts/vuln_1/summary.json, logs/attempts/vuln_2/summary.json |
| Delivered MSL script and SVG | logs/attempts/vuln_1/05_payload_env1024.msl, logs/attempts/vuln_1/06_payload_env1024.svg |
| Arbitrary write target content | logs/attempts/vuln_1/10_arbitrary_write.txt |
| Exfiltrated bytes (raw / newline-separated) | logs/attempts/vuln_1/11_exfil_blob_download.bin, logs/attempts/vuln_1/12_exfil_environ.txt |
| Planted credential recovered through the blob | logs/attempts/vuln_1/11b_exfil_canary_download.bin |
| Code execution in the HTTP response | logs/attempts/vuln_1/13_response_after_template_write.html |
| Target-local RCE marker files | logs/attempts/vuln_1/14_rce_marker.txt, logs/attempts/vuln_2/14_rce_marker.txt |
| Negative-control observations (fix commit) | logs/attempts/fixed_1/negative_control.json, logs/attempts/fixed_2/negative_control.json |
| Fixed attempt transcripts and server policy denials | logs/attempts/fixed_1/attack.stdout, logs/attempts/fixed_1/server.log |
Key excerpts (vulnerable application, attempt 1):
exfiltration channel on disk: /app-vuln/storage/fe/8k/fe8k0q5kmsjoxy2exjuo5r4a43t7
attached script_env1024.msl (1888 bytes, application/octet-stream) -> /uploads/3
delivering SVG env1024: <image xlink:href="msl:/app-vuln/storage/t3/c0/t3c0s3ff8jvzeagukdhys8grjfa1">
arbitrary write to /tmp/pruva_arbitrary_write.txt: matches attacker content
environment read with 64 samples (128 bytes) returned 1664 bytes
exfiltrated 1664 bytes through the attacker's own blob (SECRET_KEY_BASE present: True, planted credential recovered: True)
response after the template write contains command output: 'uid=0(root) gid=0(root) groups=0(root)'
server-side RCE marker file content: 'PRUVA_RCE_841517157f8b' (expected 'PRUVA_RCE_841517157f8b')
Exfiltrated environment (fragment, from logs/attempts/vuln_1/12_exfil_environ.txt):
HOSTNAME=32925638596e
RUBY_VERSION=3.4.12
PWD=/app-vuln
...
SECRET_KEY_BASE=04c5dd7e95...
Fixed application (both attempts):
exfiltrated 68 bytes through the attacker's own blob (SECRET_KEY_BASE present: False, planted credential recovered: False)
response after the template write contains command output: None
identify: attempt to perform an operation not allowed by the security policy `MSL' @ error/constitute.c/IsCoderAuthorized/454.
Environment: Linux x86_64, Docker 27.5.1, ruby:3.4-bookworm, Ruby 3.4.12, activestorage
8.0.5.1 (vulnerable worktree 7fe85a72a789eae5ee070ce3ee1dad10a8a5f70c) and the fix commit
ea8c40121ce53a3d0255cd4eb6d13bfe3bc01776 for the control, mini_magick 5.0.1 / 5.4.0,
ImageMagick 7.1.1-32 Q16-HDRI (source build, internal SVG renderer, Debian/Ubuntu-style
policy.xml that does not deny MSL), Puma, SQLite, Active Storage Disk service.
Recommendations / Next Steps
- Apply the vendor fix: upgrade
activestorageto 8.0.5.2 / 8.1.4.1 or later, which ships the ImageMagick policy and applies it throughMiniMagick.cli_env; with:mini_magick, also upgrademini_magickto 5.1 or later (older versions make the fixed release raise at boot, as observed here). - Do not rely on the host policy alone. A host policy that denies
MSLblocks the reported chain, but the advisory is explicit that it does not assume this is the only chain; the shipped policy is a denylist and the variant-analysis stage should test its completeness (coder aliases, host policy re-allow rules placed after the shipped one, GraphicsMagick, applications that replaceMiniMagick.cli_env/cli_prefixor install their ownMiniMagick::Shell). - Constrain coders, not just files: prefer
:vipsfor variant processing, and treat "attach" as an untrusted-input boundary — analysis must not run with privileges or file access beyond the blob. - Run the app without write access to its own code and without reloading in production; that removes the demonstrated write→execution path but not the write or the read.
- Testing recommendation: keep a regression test that attaches a file whose content is
<msl>-bearing SVG and asserts that no file named by the script is created.
Additional Notes
- Idempotency: the script is self-contained and was run twice consecutively; both runs
ended with
=== reproduction confirmed ===and exit status 0. Each attempt uses fresh random marker content, so the marker evidence cannot be satisfied by leftovers from an earlier run; the show template is restored from/app-srcbefore every attempt. - Limitations: (1) the RCE leg is conditional on the application loading the overwritten
path (demonstrated with code reloading enabled; see Impact Parity); (2) ImageMagick is a
source build of upstream 7.1.1-32 without librsvg, chosen because the Debian/Ubuntu packages
of the same era carry additional hardening that refuses explicit coder prefixes inside SVG
references, and because the reported chain relies on ImageMagick's own internal SVG renderer;
the shipped Debian/Ubuntu-style policy (which does not deny MSL) is installed unchanged;
(3) the environment read is attempted with progressively smaller raw sizes because ImageMagick
aborts an MSL script when a raw read runs past the end of a regular file (the payload writes
therefore run first); on this target the very first attempt returned the whole
/proc/self/environ(1664 bytes,SECRET_KEY_BASEincluded) even though 128 bytes were requested, because procfs reports no size.
GHSA-F3W6-W445-FWH9 Reproduction Transcript
The agent's step-by-step process — every tool call, every handoff, the moment the exploit fired.
Full session Replay every step — scrub the timeline or play it back.
ls -la /pruva/project-cache/ 2>&1 | head -30; echo "---MIRRORS---"; ls -la /pruva/project-cache/repo-mirrors/ 2>&1 | head; echo "---REPO---"; ls /pruva/project-cache/repo 2>&1 | head -30; echo "---ENV---"; ruby --version 2>&1; python3 --version; convert --version 2>&1 | head -3; magick --version 2>&1|head -3; docker version --format '{{.Server.Version}}' 2>&1 | head -2; echo "---POLICY---"; ls /etc/ImageMagick* 2>&1; cat /etc/ImageMagick-6/policy.xml 2>/dev/null | head -60total 4 drwx------ 4 vscode vscode 80 Nov 4 13:18 . drwxr-xr-x 3 root root 4096 Nov 4 13:18 .. drwx------ 2 vscode vscode 60 Nov 4 13:18 .pruva drwx------ 2 vscode vscode 40 Nov 4 13:18 repo-mirrors ---MIRRORS--- total 0 drwx------ 2 vscode vscode 40 Nov 4 13:18 .
echo "--- net tests ---"; curl -sS -o /dev/null -w "rubygems:%{http_code}\n" --max-time 15 https://rubygems.org/ ; curl -sS -o /dev/null -w "github:%{http_code}\n" --max-time 15 https://github.com/ ; curl -sS -o /dev/null -w "raw:%{http_code}\n" --max-time 20 https://raw.githubusercontent.com/ImageMagick/ImageMagick/main/coders/msl.c; curl -sS -o /dev/null -w "dockerhub:%{http_code}\n" --max-time 15 https://registry-1.docker.io/v2/; echo "--- docker images ---"; docker images 2>&1 | head -20; echo "--- df ---"; df -h / /pruva 2>&1 | head--- net tests --- rubygems:200 github:200 raw:200 dockerhub:401 --- docker images --- WARNING: This output is designed for human readability. For machine-readable output, please use --format. IMAGE ID DISK USAGE CONTENT SIZE EXTRA
cd /tmp && curl -sSL -o msl.c https://raw.githubusercontent.com/ImageMagick/ImageMagick/main/coders/msl.c && wc -l msl.c && grep -n 'LocaleCompare((char \*) keyword' msl.c | head -607905 msl.c
cd /tmp && grep -n 'keyword,' msl.c | head -80{"cwd":"/workspace","duration_ms":93,"exit_code":0,"stderr":"","stdout":"411: if (LocaleCompare(keyword,\"channel\") == 0)\n427: if (LocaleCompare(keyword,\"noise\") == 0)\n491: if (LocaleCompare(keyword,\"affine\") == 0)\n515: if (LocaleCompare(keyword,\"align\") == 0)\n525: if (LocaleCompare(keyword,\"antialias\") == 0)\n543: if (LocaleCompare(keyword,\"density\") == 0)\n555: if (LocaleCompare(keyword,\"encoding\") == 0)\n567: if (LocaleCompare(keyword, \"fill\") == 0)\n573: if (LocaleCompare(keyword,\"family\") == 0)\n578: if (LocaleCompare(keyword,\"font\") == 0)\n590: if (LocaleCompare(keyword,\"geometry\") == 0)\n596: if (LocaleCompare(keyword,\"gravity\") == 0)\n613: if (LocaleCompare(keyword,\"pointsize\") == 0)\n625: if (LocaleCompare(keyword,\"rotate\") == 0)\n641: if (LocaleCompare(keyword,\"scale\") == 0)\n650: if (LocaleCompare(keyword,\"skewX\") == 0)\n657: if (LocaleCompare(keyword,\… [truncated]cd /tmp && grep -n '"read"\|"write"\|"print"\|"msl"\|"image"\|"set"\|"get"\|"eval"' msl.c | sed -n '1,60p'1523: if (LocaleCompare(keyword,"image") == 0) 1626: if (LocaleCompare(keyword,"image") == 0) 3008: else if (LocaleCompare((const char *) tag,"get") == 0) 3083: if (LocaleCompare((const char *) tag,"image") == 0) 3374: if (LocaleCompare(keyword,"image") == 0) 3653: if (LocaleCompare((const char *) tag,"msl") == 0 ) 4063: if (LocaleCompare((const char *) tag,"print") == 0) 4783: if (LocaleCompare((const char *) tag,"read") == 0)
Artifacts and Evidence for GHSA-F3W6-W445-FWH9
Scripts, logs, diffs, and output captured during the reproduction.
How to Fix GHSA-F3W6-W445-FWH9
Upgrade activestorage (rubygems) · rubygems to 8.0.5.2, 8.1.4.1 or later.
FAQ: GHSA-F3W6-W445-FWH9
Is GHSA-F3W6-W445-FWH9 exploitable?
How severe is GHSA-F3W6-W445-FWH9?
What type of vulnerability is GHSA-F3W6-W445-FWH9?
Which versions of activestorage (rubygems) are affected by GHSA-F3W6-W445-FWH9?
Is there a fix for GHSA-F3W6-W445-FWH9?
How can I reproduce GHSA-F3W6-W445-FWH9?
Is the GHSA-F3W6-W445-FWH9 reproduction verified?
References for GHSA-F3W6-W445-FWH9
Authoritative sources for GHSA-F3W6-W445-FWH9 — official vulnerability databases and the upstream advisory. Pruva's reproduction verifies the issue firsthand; these are the primary records to corroborate it.