← Reports xaferima.com Español →

Stored XXE/SSRF/XSS via SVG upload sanitization bypass

XXE/SSRF/XSS CVSS 7.1 HIGH CWE-79: Cross-site Scripting snapotter-hq/SnapOtter ≤ 2.2.0

GHSA-9c7h-xjrp-8gh3 · CVE assignment pending (GitHub CNA)

Summary

Both upload routes — the standalone POST /api/v1/upload and the library POST /api/v1/files/upload — ran the SVG sanitizer only after the image validator had decoded the file:

// apps/api/src/routes/files.ts:117 (same shape at user-files.ts:337)
const safeBuffer = isValidImage && isSvgBuffer(buffer)
    ? sanitizeSvg(buffer)
    : buffer;

Because the routes accept any file type, an SVG that librsvg (via Sharp) cannot parse is stored anyway: a DOCTYPE declaring an external entity makes the parser fail, isValidImage is false, and the && short-circuit skips sanitizeSvg() entirely. The raw file is later served as image/svg+xml with its DOCTYPE, entity declarations, and any scriptable markup intact.

Two related weaknesses inside the sanitizer itself:

Impact

Root Cause

Sanitization was coupled to successful image validation instead of running on every SVG-bearing upload. The fix inverts the responsibility: both upload routes now sanitize SVG content regardless of whether the image decoder accepts the file, the sanitizer strips namespace-prefixed dangerous elements, and SVG detection plus DOCTYPE removal handle a prolog and quoted values.

Proof of Concept

Crafted SVG whose DOCTYPE makes the decoder fail, bypassing the sanitizer:



]>

  payload

The reproduction used four payloads — xss-event.svg (onload execution), xss-cookie.svg (cookie exfiltration), xxe-file-read.svg and ssrf-metadata.svg (the two vectors later ruled out by the advisory impact analysis) — uploaded with an authenticated session, then fetched via the unauthenticated download URL and opened in a browser to confirm script execution.

Proof-of-Concept Demos

Full exploit walkthrough (1:41)

End-to-end exploitation: craft malicious URL payload, trigger via API, demonstrate JavaScript execution in victim context, exfiltrate session token, achieve account takeover.

Minimal XSS payload execution (7 s)

Short clip showing the bare XSS payload javascript:alert(1) executing in the response context — confirms the reflection point without the full exploit chain.

Videos: SnapOtter SVG sanitization bypass (GHSA-9c7h-xjrp-8gh3). Full demos on PoC Demos page.

Disclosure Timeline

DateEvent
2026-09-11Vulnerability discovered during authorized audit
2026-09-12PoC and demo videos created; report sent to contact@snapotter.com
2026-09-14No acknowledgment after 48h — follow-up sent
2026-09-19Maintainer confirmed the vulnerability, classified it High, credit to xaferima
2026-09-20Downgrade to HIGH accepted (librsvg blocks XXE); CVE to be requested via GitHub CNA
2026-11-03Advisory published — GHSA-9c7h-xjrp-8gh3 — and fixed in SnapOtter v2.3.0
PendingCVE assignment via GitHub CNA

Fix

Sanitize SVG uploads unconditionally — independent of the image validator — strip namespace-prefixed dangerous elements, and harden SVG detection and DOCTYPE removal:

// Both upload routes, after the fix:
const safeBuffer = isSvgBuffer(buffer) ? sanitizeSvg(buffer) : buffer;

Fixed in SnapOtter v2.3.0 (2026-11-03, also the project's one-million-pull thank-you release, which credits this report in the release notes). Advisory published 2026-11-03; CVE assignment pending.

← Back to all reports