Skip to content

security: XSS surfaces in statute rendering (JSON-LD </script> breakout + unsanitized markdown body) #200

Description

@williamzujkowski

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    documentationImprovements or additions to documentation

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions