Skip to content

chore(dashboard): sanitize dangerouslySetInnerHTML usage#1038

Open
imparandodev[bot] wants to merge 1 commit into
mainfrom
chore/sanitize-dangerous-html
Open

chore(dashboard): sanitize dangerouslySetInnerHTML usage#1038
imparandodev[bot] wants to merge 1 commit into
mainfrom
chore/sanitize-dangerous-html

Conversation

@imparandodev
Copy link
Copy Markdown
Contributor

@imparandodev imparandodev Bot commented Apr 30, 2026

Summary

Three call sites in dashboard/ were piping strings into dangerouslySetInnerHTML. Two of them (the bootstrap banner on AsyncRequests.tsx and Batches.tsx) read from window.bootstrapContent, which today comes from a same-origin static asset (public/bootstrap.js) — but treating that as inherently safe is a brittle assumption. The third (ui/chart.tsx) generates a <style> block from chart config, interpolating id, key, and color strings directly into CSS — a CSS-injection vector if any of those become attacker-controlled.

This PR closes those vectors:

  • New components/ui/safe-html.tsx — a <SafeHTML html=... /> wrapper that runs the input through DOMPurify before passing to dangerouslySetInnerHTML. Configured to allow <style> (the banner uses inline CSS) plus FORCE_BODY: true so a leading <style> survives sanitization, and explicitly forbids script/iframe/object/embed plus inline event handler attrs.
  • AsyncRequests.tsx and Batches.tsx — replace raw <div dangerouslySetInnerHTML /> with <SafeHTML html=... />.
  • ui/chart.tsx — validate id, config keys and color values against strict ^[A-Za-z0-9_-]+$ (identifier) and ^[A-Za-z0-9 ,.()#%/_-]+$ (color) regexes; drop any entry that fails. The <style> tag now only ever contains values that survive that filter.
  • New safe-html.test.tsx covering: allowed markup pass-through, <script> stripping, inline-event-handler stripping, javascript: URL stripping, and <style> preservation (required for the banner to render correctly).
  • New dependency: dompurify ^3.4.1 (ships its own types).

Threat model addressed

  • Bootstrap banner: if bootstrap.js is ever swapped for an operator-customised file, fetched from an API, or compromised in transit, sanitization keeps the resulting markup within a known-safe profile rather than running arbitrary JS.
  • Chart <style>: if a ChartConfig ever ends up partially derived from user input (label fragments, search terms, etc.), the regex filters prevent CSS-rule break-out (}; @import url(evil)) and value-based exfiltration tricks.

Local verification

  • just lint ts — ✅ clean
  • just test ts — ✅ 503 / 503 passing (498 prior + 5 new SafeHTML tests)
  • pnpm build — ✅ Vite production build succeeds

Test plan

  • just lint ts passes in CI
  • just test ts passes in CI
  • Frontend production build job succeeds
  • Manual smoke: open /async and /batches with the banner enabled — banner renders identical to main
  • Manual smoke: a chart renders with expected colours via --color-* CSS vars

The bootstrap banner rendered in AsyncRequests and Batches passed an HTML
string from window.bootstrapContent (sourced from public/bootstrap.js)
straight into dangerouslySetInnerHTML. Even though that source is
same-origin and dev-controlled today, that's a fragile assumption: any
future fetch-based or operator-customised replacement would silently
become an XSS sink.

Introduce a SafeHTML wrapper that runs DOMPurify on the input before
rendering, allowing only the markup the banner needs (style, basic
layout, SVG icons) and explicitly forbidding script/iframe/object/embed
plus inline event handlers. FORCE_BODY is enabled so a leading <style>
in the input survives sanitisation.

Wire the wrapper into both banner sites, and harden the chart.tsx
<style> generator (also a dangerouslySetInnerHTML sink) by validating
chart ids, config keys, and color values against strict CSS-identifier
and CSS-color regexes before interpolating them into the rule. Anything
that doesn't match is dropped, which closes the CSS-injection vector
that an attacker-controlled config object could otherwise exploit.

Add focused tests for SafeHTML covering: allowed markup pass-through,
script/event-handler/javascript: URL stripping, and <style> preservation
(needed for the banner). Full ts test suite + production build pass
locally.
@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying control-layer with  Cloudflare Pages  Cloudflare Pages

Latest commit: f7172a3
Status: ✅  Deploy successful!
Preview URL: https://90c2be93.control-layer.pages.dev
Branch Preview URL: https://chore-sanitize-dangerous-htm.control-layer.pages.dev

View logs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants