Severity: HIGH — found during security review of apps/web
Statute content (frontmatter title/classification, markdown body) originates from OLRC XML→markdown. Treating it as untrusted (defense-in-depth; the build ingests external government data), two sinks render it unsafely.
H1 — JSON-LD </script> breakout (CONFIRMED mechanism)
apps/web/src/pages/statute/[...slug].astro:147
<script type="application/ld+json" set:html={JSON.stringify({ name: classification, headline: entry.data.title.replace(...), ... })} />
JSON.stringify does not escape <, >, /. A statute title/classification containing </script><img src=x onerror=alert(1)> terminates the script element; the remainder parses as live HTML. set:html adds no escaping.
Fix: escape after stringify — JSON.stringify(obj).replace(/</g, '\\u003c') (standard safe-JSON-in-<script> pattern; still valid JSON-LD).
H2 — statute markdown body rendered unsanitized (NEEDS-VERIFICATION on transformer)
apps/web/src/pages/statute/[...slug].astro:309 and apps/web/src/pages/browse/[title]/[chapter].astro:169 render <Content /> with no rehype-sanitize and no sanitize-html on the body. Astro passes embedded raw HTML through by default. If the XML→markdown transformer ever emits/relays raw HTML (<img onerror>, <iframe>, event handlers), it renders live. The repo already uses sanitize-html for GitHub data and search excerpts but not for the primary statute body.
Fix: add rehype-sanitize to the markdown config, or sanitize transformer output, or assert HTML-free transformer output (verify which guarantee holds).
M1 — CSP script-src 'unsafe-inline' negates its own anti-XSS value
apps/web/src/layouts/BaseLayout.astro:56 — the CSP is commented as "defense-in-depth against XSS" but 'unsafe-inline' permits inline onerror=/onclick= and inline <script>, so it does not stop H1's injected handler. The site uses many is:inline scripts, so tightening requires nonces/hashes. Either fix H1/H2 at the source (don't rely on CSP) or migrate to hashed/nonce'd scripts so 'unsafe-inline' can be dropped.
Clean (reviewed, no action)
Octokit/token handling (no token reaches client, GitHub data escaped/sanitized), RSS escaping (@astrojs/rss escapes standard fields), SearchBar {@html} (tight <mark>-only allowlist), getStaticPaths slug uniqueness, no eval/unsafe dynamic import.
Severity: HIGH — found during security review of apps/web
Statute content (frontmatter
title/classification, markdown body) originates from OLRC XML→markdown. Treating it as untrusted (defense-in-depth; the build ingests external government data), two sinks render it unsafely.H1 — JSON-LD
</script>breakout (CONFIRMED mechanism)apps/web/src/pages/statute/[...slug].astro:147JSON.stringifydoes not escape<,>,/. A statutetitle/classificationcontaining</script><img src=x onerror=alert(1)>terminates the script element; the remainder parses as live HTML.set:htmladds no escaping.Fix: escape after stringify —
JSON.stringify(obj).replace(/</g, '\\u003c')(standard safe-JSON-in-<script>pattern; still valid JSON-LD).H2 — statute markdown body rendered unsanitized (NEEDS-VERIFICATION on transformer)
apps/web/src/pages/statute/[...slug].astro:309andapps/web/src/pages/browse/[title]/[chapter].astro:169render<Content />with no rehype-sanitize and nosanitize-htmlon the body. Astro passes embedded raw HTML through by default. If the XML→markdown transformer ever emits/relays raw HTML (<img onerror>,<iframe>, event handlers), it renders live. The repo already usessanitize-htmlfor GitHub data and search excerpts but not for the primary statute body.Fix: add
rehype-sanitizeto the markdown config, or sanitize transformer output, or assert HTML-free transformer output (verify which guarantee holds).M1 — CSP
script-src 'unsafe-inline'negates its own anti-XSS valueapps/web/src/layouts/BaseLayout.astro:56— the CSP is commented as "defense-in-depth against XSS" but'unsafe-inline'permits inlineonerror=/onclick=and inline<script>, so it does not stop H1's injected handler. The site uses manyis:inlinescripts, so tightening requires nonces/hashes. Either fix H1/H2 at the source (don't rely on CSP) or migrate to hashed/nonce'd scripts so'unsafe-inline'can be dropped.Clean (reviewed, no action)
Octokit/token handling (no token reaches client, GitHub data escaped/sanitized), RSS escaping (@astrojs/rss escapes standard fields), SearchBar
{@html}(tight<mark>-only allowlist), getStaticPaths slug uniqueness, no eval/unsafe dynamic import.