Fix #014: sanitize themed text kinds + script-blocking CSP (stored XSS)

markdown/deck/html_fragment rendered author-influenced content verbatim,
so <script>/<img onerror>/etc. executed on the Daybook origin — self-XSS
against the owner's session, and (post-sharing) unauthenticated via /s/.

Two layers, both in artifacts/views.py:
- Create-time allowlist sanitization via nh3 (maintained ammonia/Rust
  sanitizer, not deprecated bleach). Keeps formatting/tables/links/images
  (incl. relative /assets URLs) and codehilite markup; drops script, on*
  handlers, style, unknown tags, and javascript:/data: URLs. html_full is
  deliberately NOT sanitized (documented verbatim escape hatch).
- Serve-time CSP (script-src 'none') for the sanitized kinds, on both
  /a/{id} and the unauthenticated /s/{token} route. Being a response
  header, it also neutralizes scripts in artifacts stored before this fix.
  Not applied to html_full or json/csv (json/csv data is already escaped;
  their viewers need their own JS).

Verified live across all kinds; 7 regression tests added. 102 tests pass.

Not addressed (noted in the issue): CSV formula injection,
CSRF_COOKIE_HTTPONLY (moot with XSS closed), nonce CSP for json/csv/full.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
peter 2026-09-03 10:44:49 +01:00
parent 6d2fe2147c
commit e3c5773e8f
6 changed files with 196 additions and 19 deletions

View file

@ -28,10 +28,10 @@ Currently supported kinds:
| Kind | Upload type | Rendered behavior | Raw support |
|---|---|---|---|
| `html_fragment` | JSON text | Wrapped with a Daybook theme; tables styled automatically | No |
| `html_full` | JSON text | Stored as complete HTML, verbatim — no theme or styling injected | Yes |
| `markdown` | JSON text | Rendered to HTML (code blocks syntax-highlighted, tables styled) and original `.md` preserved | Yes |
| `deck` | JSON text | Wrapped as a simple HTML slide/deck page | No |
| `html_fragment` | JSON text | Wrapped with a Daybook theme; tables styled automatically; **HTML sanitized** | No |
| `html_full` | JSON text | Stored as complete HTML, verbatim — no theme, styling, or sanitization | Yes |
| `markdown` | JSON text | Rendered to HTML (code blocks syntax-highlighted, tables styled), **sanitized**, original `.md` preserved | Yes |
| `deck` | JSON text | Wrapped as a simple HTML slide/deck page; **HTML sanitized** | No |
| `image` | Multipart file | Browser image preview | No |
| `pdf` | Multipart file | Browser PDF preview | No |
| `binary` | Multipart file | Stored and downloadable | No |
@ -42,6 +42,22 @@ Currently supported kinds:
though the value itself is JSON/CSV text — i.e. `{"kind": "json", "content":
"{\"a\": 1}"}`, not a nested JSON object in the request body.
### HTML sanitization
The "themed" kinds — `markdown`, `html_fragment`, `deck` — have their
rendered HTML run through an allowlist sanitizer (nh3) before storage.
Ordinary formatting, tables, links, images (including relative
`/assets/<id>` URLs), and code-highlight markup are preserved; `<script>`,
event handlers (`on*`), `<style>`, `javascript:`/`data:` URLs, and unknown
tags are dropped. These kinds are also served with a `script-src 'none'`
Content-Security-Policy. As a side effect, sanitized output is always
well-formed HTML.
`html_full` is the deliberate exception: it is stored and served **exactly**
as posted, with no sanitization and no restrictive CSP. It is the
"poster owns all presentation, including any scripts and the security
implications" escape hatch — only post trusted content as `html_full`.
## Source Fields
Every artifact can identify who or what produced it. `source` is a free-text label —