GHSA-97v6-998m-fp4g ApostropheCMS: Stored XSS via CSS Custom Property Injection in @apostrophecms/color-field Escaping Style Tag Context
SummaryThe `@apostrophecms/color-field` module bypasses color validation for values prefixed with `--` (intended for CSS custom properties), but performs no HTML sanitization on these values. When styles containing attacker-controlled color values are rendered into `<style>` tags — both in the global stylesheet (editors only) and in per-widget style elements (all visitors) — the lack of escaping allows an editor to inject `</style>` followed by arbitrary HTML/JavaScript, achieving stored XSS against all site visitors.Details**Root Cause 1: Validation bypass in color field** (`modules/@apostrophecms/color-field/index.js:36`)The color field's `convert` method uses TinyColor to validate color values, but exempts any value starting with `--`:A value like `--x: red}</style><script>alert(document.cookie)</script><style>` passes validation because it starts with `--`. The `launder.string()` call performs type coercion only — it does not strip HTML metacharacters like `<`, `>`, or `/`.**Root Cause 2a: Unescaped rendering in widget styles (public path)** (`modules/@apostrophecms/styles/lib/methods.js:232-234`)The `getWidgetElements()` method concatenates the CSS string directly into a `<style>` tag:This is then marked as safe HTML via `template.safe()` in the helpers (`modules/@apostrophecms/styles/lib/helpers.js:17-20`), and rendered for **all visitors** on any page containing a styled widget (`modules/@apostrophecms/widget-type/index.js:426-432`).**Root Cause 2b: Unescaped rendering in global stylesheet (editor path)** (`modules/@apostrophecms/template/index.js:1164-1165`)The `renderNodes()` function returns `node.raw` without escaping:Style nodes containing the malicious color values are rendered as raw HTML, affecting editors and admins who can `view-draft`.PoC**Prerequisites:** An account with `editor` role on an Apostrophe 4.x instance. The site must have at least one piece or page type with a color field used in styles configuration.**Step 1: Authenticate and obtain a CSRF token and session cookie.****Step 2: Create or update a piece/page with a malicious color value in a styled widget.**The exact API route depends on the site's widget configuration. For a widget type that uses a color field in its styles schema (e.g., a `background-color` style property):**Step 3: Publish the page.****Step 4: Any visitor navigates to the published page.****Expected (safe):** The color value is escaped or rejected.**Actual:** The rendered HTML contains:The injected `</style>` closes the style tag, and the `<img onerror>` executes JavaScript in the visitor's browser.Impact**Stored XSS on public pages (Path B):** An editor can inject JavaScript that executes for **every visitor** to any page containing the affected widget. This enables mass cookie theft, session hijacking, keylogging, phishing overlays, and drive-by malware delivery against the site's entire audience.**Privilege escalation (Path A):** An editor can steal admin session tokens from higher-privileged users viewing draft content, escalating to full administrative control of the CMS.**Persistence:** The payload is stored in the database and survives restarts. It executes on every page load until the content is manually edited.**No CSP mitigation:** Apostrophe does not enforce a strict Content-Security-Policy by default, so inline script execution is not blocked.Recommended Fix**Fix 1: Sanitize color values in the color field's `convert` method** (`modules/@apostrophecms/color-field/index.js`):**Fix 2: Escape CSS output in `getWidgetElements`** (`modules/@apostrophecms/styles/lib/methods.js`):Both fixes should be applied: Fix 1 provides input validation (defense in depth at the data layer), and Fix 2 provides output encoding (preventing style tag breakout regardless of the input source).
CVSS v3.1 score: 5.4