id: 4ab82678-f954-4bef-bfbc-eed37ab38bde title: 'Stored XSS: raw/<path> serves committed HTML/SVG as text/html' state: closed labels:
- security assignees: [] milestone: null created: 2026-10-27T14:53:32.418656561Z updated: 2026-10-27T15:44:20.851821135Z
dist/raw/<path> is byte-identical repo content served with a content type chosen from the extension, so any committed .html (or .svg, .xhtml, .xml) executes script on the site's origin.
Verified:
$ printf '<!doctype html><script>fetch("/inbox/issue",{method:"POST"})</script>hi\n' > evil.html
$ hu13 build . -o out && hu13 serve --www out --inbox-dir /tmp/ib
$ curl -D- http://127.0.0.1:39501/raw/evil.html
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
...
<!doctype html><script>fetch("/inbox/issue",{method:"POST"})</script>hi
files/evil.html.html is correctly escaped (<script>), so the escaped view is fine — the raw view is the hole. It needs no click: visiting the URL runs it.
Why it matters here specifically: the page is same-origin with /inbox/*, so injected script satisfies the Origin gate, can POST spam from the visitor's own IP, has full access to the DOM of the mirror (phishing/defacement), and can chain the inbox forms. It also breaks the project invariant "bodies from the repo are treated as untrusted".
Same hole exists on Cloudflare Pages (it also types by extension), so this cannot be fixed in serve alone.
Fix direction:
- serve
/raw/**asapplication/octet-stream+Content-Disposition: attachment; filename="..."for anything that browsers render (html/htm/svg/xhtml/xml/xsl/mht),text/plain; charset=utf-8for text files - never
text/html,image/svg+xmlorapplication/xhtml+xmlunder/raw/ - Pages: emit
_headerswith the same rule (or drop theraw/tree from Pages builds) - optionally add
Content-Security-Policy: default-src 'none'; style-src 'self'on raw responses as belt-and-braces
Acceptance:
-
GET /raw/x.htmlis nevertext/htmland does not execute script -
text files still render readably as
text/plain -
a test builds a repo containing
<script>in.html/.svgand asserts the served content type