---
id: 7b0b0eae-93d2-4cb3-8fa5-e01cbf1447ac
title: 'Markdown link/image URLs are not scheme-filtered (javascript: XSS)'
state: closed
labels:
- security
assignees: []
milestone: null
created: 2026-10-27T14:53:39.317738326Z
updated: 2026-10-27T16:04:34.720653453Z
---
Markdown link and image destinations are emitted verbatim, so `[click](javascript:...)` renders as a live `javascript:` link on the mirror.

Verified with a committed README:

```
[click me](javascript:alert(document.domain))
![img](javascript:alert(2))
```

```
$ grep -o '<a href="[^"]*"' out/index.html
<a href="javascript:alert(document.domain)"
$ grep -o '<img src="[^"]*"' out/index.html
<img src="javascript:alert(2)"
```

`markdown_to_html` intercepts `Event::Html` / `Event::InlineHtml` and escapes them (good — raw `<script>` in markdown is neutralized), but `Tag::Link { dest_url }` / `Tag::Image { dest_url }` pass through untouched. pulldown-cmark does not sanitize URLs by design.

Impact: stored XSS one click away from README, wiki pages, issue/PR bodies and release notes — same origin as `/inbox/*`, so any injected script can post submissions with the visitor's IP. Complements #61 (which needs no click) — fixing both closes the pair.

Fix: keep intercepting events in `markdown_to_html` and add a `Tag` arm that rewrites `Link`/`Image` destinations through an allowlist:
- allow `http:`, `https:`, `mailto:`, and relative/fragment/`//host` forms
- everything else (javascript:, data:, vbscript:, file:) renders with an empty/`#` href or as plain text
- note `esc` does not percent-encode, so decide explicitly whether to encode the URL (currently `#`/`?`/space in a filename break the link, a separate correctness nit)

Acceptance:
- [ ] `[x](javascript:alert(1))` never reaches an `href`/`src`
- [ ] `data:` and `vbscript:` likewise
- [ ] legit links (relative files, https, mailto) unchanged
- [ ] unit test on `markdown_to_html` covering each scheme
