---
id: 69c0303b-0dcd-4b1b-a67c-04bcfdde011f
title: release artifacts are served as active HTML/SVG (stored XSS)
state: closed
labels:
- kind:security
assignees: []
milestone: null
created: 2026-10-27T18:08:25.218070362Z
updated: 2026-10-27T18:20:51.719490015Z
---
## What

Release artifacts under `.forge/releases/out/<version>/` are copied verbatim to `dist/releases/<version>/`, but they are served as active documents:

- `src/releases.rs:462-465` copies the whole tree.
- `src/serve.rs:729-733` only special-cases `raw/`; an artifact named `evil.html` gets `Content-Type: text/html` and `inline` disposition.
- `src/build.rs:1150` `_headers` protects only `/raw/*`; Pages serves the artifact inline too.

## Reproduced

Fixture repo:

```
mkdir -p .forge/releases/out/v1.0
cat > .forge/releases/out/v1.0/evil.html <<'HTML'
<!doctype html><script>document.body.textContent="PWNED"</script>
HTML
```

Build, serve, then:

```
curl -I http://127.0.0.1:PORT/releases/v1.0/evil.html
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
```

The script executes in the site origin (the origin that hosts the inbox).

## Impact

Stored XSS via a release artifact (HTML/SVG/etc.), on `hu13 serve` and Cloudflare Pages.

## Fix

- Treat `/releases/<version>/...` artifacts as downloads in `serve` (same policy as raw) and add a Pages `_headers` rule, e.g. `/releases/*/*` -> `Content-Disposition: attachment`, `X-Content-Type-Options: nosniff`.
- Keep `/releases/<version>.html` itself inline.
- Regression test: artifact `.html`/`.svg` response is attachment and `application/octet-stream`/safe MIME.
