Skip to content

feat(core): sanitize Portable Text HTML output to prevent stored XSS #644

@Vallhalen

Description

@Vallhalen

Problem

EmDash renders Portable Text content using Astro's `set:html` directive without sanitization. If an attacker gains access to the CMS (e.g., via a leaked API token), they can inject malicious HTML/JavaScript into content blocks that will be rendered to all visitors.

Components affected (example from a real site):

  • `About.astro` - `set:html` with CMS data
  • `CtaBanner.astro` - `set:html` with CMS data
  • `ValueProps.astro` - `set:html` with CMS data
  • Any component using `` or `set:html` with user-managed content

Security impact

This was identified during a security audit. While Portable Text is structured (not raw HTML), the rendering pipeline converts blocks to HTML and injects via `set:html`. If CMS content is compromised:

  • Stored XSS affecting all site visitors
  • Session hijacking
  • Phishing overlays
  • Crypto mining scripts

Proposed solution

Add built-in HTML sanitization at the SSR rendering stage:

  1. PortableText component: sanitize output before `set:html` injection (e.g., using DOMPurify or a lightweight allowlist)
  2. Configurable allowlist: let site owners define which HTML tags/attributes are permitted
  3. CSP nonce support: optionally inject nonces for inline scripts that EmDash itself generates

Current workaround

Developers must manually wrap CMS content with a sanitizer:

import DOMPurify from "isomorphic-dompurify";
const clean = DOMPurify.sanitize(htmlFromCMS);

But this requires knowing which components use `set:html` and adds a dependency.

Environment

  • EmDash 0.5.0
  • Astro 6 with SSR
  • Cloudflare Workers deployment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions