# Snipe-IT: Stored XSS via Inline XML Rendering in the Uploaded Files API

Snipe-IT API serves uploaded XML inline without the safe-inline allowlist, letting an authenticated uploader run XSLT-driven JavaScript in a victim's origin.

- Payout: Research
- Severity: high
- Program: Snipe-IT
- Found by: B1gN0Se, Rajib-Mahmud
- Disclosed: 2026-09-24
- CVE: CVE-2026-63498
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-396x-xmvh-p563
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-396x-xmvh-p563
- Tags: area:web domain:api domain:web impact:ato impact:privesc kind:research platform:ghsa sev:high tech:php technique:source-review vuln:file-upload vuln:xss

**Target:** Uploaded-files API download endpoint (GET /api/v1/{object_type}/{id}/files/{file_id})
**Root cause:** Upload validation allows .xml attachments and only sanitizes files detected as image/svg+xml, while the API show() honors attacker-controlled inline=true and returns Content-Disposition: inline without calling StorageHelper::allowSafeInline(). Laravel serves the file as text/xml, so Chromium applies the xml-stylesheet XSLT and renders attacker HTML/JS in the Snipe-IT origin.
**Steps:**
1. Upload XSLT stylesheet as `style.xml` via `curl -F 'file[]=@style.xml;type=text/xml' https://snipe-it.example/api/v1/models/1/files` with attacker Bearer token; finfo detects text/xml so no SVG sanitization applies.
2. List files via `GET /api/v1/models/1/files` and read the stylesheet's id (STYLE_FILE_ID).
3. Upload `data.xml` containing `<?xml-stylesheet type="text/xsl" href="https://snipe-it.example/api/v1/models/1/files/STYLE_FILE_ID?inline=true"?>` and note its id (DATA_FILE_ID).
4. As authenticated victim, open `https://snipe-it.example/api/v1/models/1/files/DATA_FILE_ID?inline=true`; response is `Content-Type: text/xml`, `Content-Disposition: inline`, CSP with `script-src 'self' 'unsafe-inline' 'unsafe-eval'`.
5. Observe title change to SNIPE_XSS and text NOT_EXECUTED -> XSS_EXECUTED, confirming script execution in the Snipe-IT origin.
**How found:** Compared the API UploadedFilesController::show() against the non-API controller, noticing the latter calls StorageHelper::allowSafeInline() before inline responses while the API path does not; then checked UploadFileRequest sanitization (SVG-only) and finfo typing of XML as text/xml to confirm the bypass.
**Exploitation:** Attacker with an API token and file-attach permission on any supported object uploads two XML attachments (XSLT + referencing doc). Victim with view rights opens the data.xml URL with ?inline=true; the browser fetches the same-origin stylesheet attachment and executes the transform's inline script under the victim's session, enabling same-origin reads of API responses and state-changing requests as the victim (superuser compromise if an admin opens it).
**Escalation:** Impact scales with victim privilege: superuser viewing the attachment yields administrative account compromise; HttpOnly cookies don't block authenticated same-origin fetches.
**Insight:** When a framework derives Content-Type from stored file content, extension allowlists are not a safety signal: audit every code path that sets Content-Disposition: inline and diff API vs web controllers for missing allowSafeInline()-style gates. XML with xml-stylesheet is an active-content vector even when SVG sanitization exists.

