Skip to content

chore(deps): update dependency sanitize-html to v2.18.0 [security] - #957

Merged
Danathar merged 2 commits into
mainfrom
renovate/npm-sanitize-html-vulnerability
Oct 23, 2026
Merged

Danathar merged 2 commits into
mainfrom
renovate/npm-sanitize-html-vulnerability

Conversation

@mergeraptor

@mergeraptor mergeraptor Bot commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

This PR contains the following updates:

Package Change Age Confidence
sanitize-html (source) 2.17.7 → 2.18.0 age confidence

Warning

Some dependencies could not be looked up. Check the Dependency Dashboard for more information.


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

CVE-2026-113657 / GHSA-374f-7chj-9948

More information

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

Severity

  • CVSS Score: 3.7 / 10 (Low)
  • Vector String: CVSS:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


ApostropheCMS: Incomplete CVE-2026-53606: meta http-equiv=refresh content still ships javascript: / data: URLs

CVE-2026-113661 / GHSA-cv27-6wvh-8x7j

More information

Details

Summary

CVE-2026-53606 expanded allowedSchemesAppliedToAttributes so named URL attributes (action, formaction, data, poster, background, later ping, xlink:href, …) go through naughtyHref(). A URL nested inside meta content (http-equiv=refresh) is not an attribute name on that list, so the embedded destination is never scheme-checked.

Default allowedTags still omit meta. Same non-default integrator config as 53606: allow meta plus http-equiv / content.

On 2.17.7:

sanitizeHtml(
  '<meta http-equiv="refresh" content="0;url=javascript:alert(1)">',
  { allowedTags: ['meta'], allowedAttributes: { meta: ['http-equiv', 'content'] } }
)
// => <meta http-equiv="refresh" content="0;url=javascript:alert(1)" />

data:text/html,… and arbitrary https: destinations also survive. formaction="javascript:…" (53606) is stripped.

Please Request CVE. Credit: Finder + Reporter (adrbogacz).

Details

naughtyHref() only runs when the attribute name is in allowedSchemesAppliedToAttributes (index.js ~385). Refresh URLs live in a content value of the form 0;url=…, so the gate never fires. Adding content to that list wholesale would break ordinary meta content (description, viewport).

Fix: when http-equiv is refresh (case-insensitive), parse the ; url= destination and run naughtyHref('meta', url). Drop content (or the tag) if the URL is rejected. Leave other meta content values alone.

PoC

Local sanitize-html@2.17.7. Output inspection only:

node -e '
const sanitize = require("sanitize-html");
const opts = { allowedTags: ["meta"], allowedAttributes: { meta: ["http-equiv", "content"] } };
for (const html of [
  "<meta http-equiv=\"refresh\" content=\"0;url=javascript:alert(1)\">",
  "<meta http-equiv=\"refresh\" content=\"0;url=data:text/html,aaa\">",
  "<form><button formaction=\"javascript:alert(1)\">x</button></form>",
]) {
  console.log(html, "=>", sanitize(html, html.includes("meta") ? opts : { allowedTags: ["form","button"], allowedAttributes: { button: ["formaction"] } }));
}
'

Expected: refresh javascript: / data: remain; 53606 formaction is removed. Do not attach a browser hang or stored-XSS page.

Impact

An integrator who allowed meta + content expecting allowedSchemes to apply (the 53606 configuration pattern) still emits a refresh to javascript:, data:, or an attacker https: URL. Stored XSS / open redirect for viewers of that HTML. Default config is not affected.

Severity

  • CVSS Score: 5.4 / 10 (Medium)
  • Vector String: CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


ApostropheCMS sanitize-html: nonTextTags discard region ends early on an implied close, letting content render

CVE-2026-113654 / GHSA-x3q4-9hxx-gx8m

More information

Details

Summary

sanitize-html discards the content of every element in nonTextTags. It delimits that region
using htmlparser2's close events, and ignores the isImplied flag htmlparser2 passes it. Closing a
still-open ancestor from inside a <noscript> makes htmlparser2 unwind its stack past the
noscript; the synthetic implied close it emits on the way ends the discard region early, and
everything after it is treated as ordinary body content.

A browser with scripting enabled parses <noscript> content as raw text, so the same markup
renders as inert text there. The sanitizer therefore emits elements the page itself would not have.

Reproduction
const sanitizeHtml = require("sanitize-html");

sanitizeHtml('<div><noscript></div><img src="http://evil.test/t.gif"></noscript>', {
  allowedTags: ["div", "img"],
  allowedAttributes: { img: ["src"] },
  nonTextTags: ["script", "style", "textarea", "option", "noscript"],
});
// => '<div></div><img src="http://evil.test/t.gif" />'

Chrome, given the same input, renders no <img> — the whole payload is raw text inside the
unclosed <noscript>. Verified with Playwright, srcdoc into an iframe so the scripting flag is on.

A <![CDATA[ variant hides the opening tag from the parser entirely and has the same effect:

<p>Hi</p><![CDATA[ x > <noscript>]]><a href="…">Reset your password</a><img src="…beacon.gif"></noscript>

htmlparser2 runs a CDATA token to ]]>; Chrome ends a bogus comment at the first >, so the
<noscript> after it is real to the browser and invisible to the parser.

Where the behaviour is

index.js:

  • :154 — nonTextTagsArray (the default list, ['script','style','textarea','option','xmp']).
  • :638 — onclosetag: function (name, isImplied) — receives the flag.
  • :644 — skipTextDepth-- — decrements without consulting isImplied.
Suggested fix

Do not end a discard region on an implied close. htmlparser2 already distinguishes them; the flag
is passed and discarded. Something close to:

onclosetag: function (name, isImplied) {
  if (skipText) {
    if (isImplied) return;   // an implied close must not end the region
    skipTextDepth--;
    …

Severity

  • CVSS Score: 5.3 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Release Notes

apostrophecms/apostrophe (sanitize-html)

v2.18.0

Compare Source

Adds
  • Added a logger option: pass any console-shaped object, with debug, info, warn and error methods, and sanitize-html's own diagnostics are delivered to it rather than to the console, so an application with a logging pipeline of its own can route them. Missing methods, and no option at all, fall back to the console. Those messages also lost their decorative line breaks and warning icon, so each is now a single line of text; their wording is otherwise unchanged.
Fixes
  • allowedSchemesByTag is now applied to srcset and imagesrcset URLs. Previously the per-tag lookup used the attribute name instead of the tag name, so these attributes always fell back to the global allowedSchemes and ignored a tag-specific scheme allowlist. Thanks to
    spokodev for the fix.
  • Starting in version 2.17.6, sanitize-html began escaping any markup preserved inside a disallowed iframe tag, which was a change
    in behavior due to an upstream change in htmlparser2. This fix ensures such "fallback markup" is preserved without escaping, but also
    fully sanitized according to the same rules as the original input. Thanks to sumitjhacodes for
    the fix.
Security
  • When meta was allowed together with its http-equiv and content attributes, the destination URL of a <meta http-equiv="refresh" content="0;url=..."> was never checked against allowedSchemes, because it is embedded in content rather than being an attribute of its own. So javascript:, data: and other disallowed destinations passed through. The refresh URL is now extracted the way browsers do it, allowing for the different spellings, separators, quoting and letter case of url=, and checked against allowedSchemes (or allowedSchemesByTag.meta). If it is rejected, or the content cannot be parsed as a refresh, the content attribute is removed. content on other meta elements is unchanged. The default configuration does not allow meta and was not affected (CWE-79, CWE-601, GHSA-cv27-6wvh-8x7j).

    Thanks to adrbogacz for reporting the vulnerability.

  • When noscript is listed in nonTextTags, the discarded region could end too early. Browsers with scripting enabled treat <noscript> content as raw text up to the first </noscript>, but the underlying parser treats it as markup, so an end tag for an enclosing element inside <noscript> closed it implicitly and the rest of its content was emitted as ordinary sanitized markup. The discard region now continues until the point where a browser would end the <noscript> element, while implied closes of other nonTextTags such as <option> behave as before (CWE-79, CWE-436, GHSA-x3q4-9hxx-gx8m).

    Thanks to joaquiniglesiaslug for reporting the vulnerability.

  • The check that drops SVG animation elements (animate, animateColor, animateMotion, animateTransform, set) when they retarget a URL attribute such as href compared the full tag name, so a namespace-prefixed spelling like svg:animate was not recognized when such tags were allowed (for example with allowedTags: false). In XML serializations such as XHTML or standalone SVG, the prefixed element is a real animation element and could retarget a link to a javascript: URL after sanitization. The element and attributeName are now matched by their local names, ignoring any prefix (CWE-79, CWE-184, GHSA-374f-7chj-9948).

    Thanks to Kai Aizen (SnailSploit) for reporting the vulnerability.


Configuration

📅 Schedule: (UTC)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Enabled.

♻ Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR has been generated by Mend Renovate CLI.

@hivecommons-hive

Copy link
Copy Markdown
Contributor
0172fef

⚠️ Sentinel alert — maintainer review required

Hive flagged this PR (author @mergeraptor[bot], head 0172fef492ea) because it matches behaviors that can override security controls, escalate privileges or damage the codebase. This is a heuristic, not an accusation — a maintainer should confirm the change is intended before it merges.

  • sensitive_path — changes 1 sensitive path(s) (Touches a sensitive path (OWNERS, workflows, policies, hive config, security docs, …))
    • package-lock.json

Hive added the sentinel-alert label. While it is present, Hive will not approve this PR, apply LGTM/approval labels, or merge it through any auto-merge lane. Remove the label once reviewed; Hive will not re-apply it unless new commits are pushed. Tune paths and behaviors under sentinel in hive.yaml or the dashboard Security tab.

@hivecommons-hive hivecommons-hive Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary: This automated update moves the website's HTML-cleaning library (sanitize-html, which scrubs dangerous markup out of community-translated text before it is shown in the browser) from 2.17.7 to 2.18.0 to pick up several security fixes. The version it lands on, however, has its own published flaw: specially nested markup can crash the cleaner. The library's authors already shipped 2.18.1 six days ago fixing that. The update should target 2.18.1 instead.
What I need from you: Re-run the bot against sanitize-html 2.18.1 (or bump the lockfile manually).


security

  • medium (in-scope) — package-lock.json:7854 pins sanitize-html to 2.18.0. Upstream CHANGELOG.md (2.18.1, 2026-10-14) records GHSA-c9rx-x947-fp3h: "Since 2.18.0, the fallback markup of a discarded <iframe> was re-sanitized by a recursive call with no limit … A few kilobytes of crafted input could throw RangeError: Maximum call stack size exceeded with the default options … Only 2.18.0 was affected." This site runs the sanitizer with sanitize.defaults.allowedTags (src/utils/markdown.ts:12-13), so <iframe> is a discarded tag and the affected path is live. Exploitability here is low — input is community-contributed locale strings merged via PR (src/utils/markdown.ts:5-6), and the sanitizer runs in the browser bundle, so impact is a client-side crash on a page, not server DoS — but landing a version with an open advisory when sanitize-html@2.18.1 is already on npm defeats the purpose of a [security] bump. Fix: update the lock to 2.18.1 (the ^2.17.7 range in package.json:44 already permits it). Note 2.18.1 also drops the deepmerge dependency, so the lock diff will shrink.

No findings from: correctness, intent-alignment, style, docs-currency.

Confidence: 3/5 (needs attention) — 1 medium finding; 1 perspective requested changes

— hive: agent=reviewer backend=copilot model=claude-fable-5.1 copilot=1.0.88

2.18.0 is superseded by 2.18.1 (GHSA-c9rx-x947-fp3h); 2.18.1 also
drops the deepmerge dependency.

Hive-Fix-Attempt: 1/6
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: scanner <280983584+hivecommons-hive[bot]@users.noreply.github.com>

@hivecommons-hive hivecommons-hive Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary: This update bumps the library the site uses to scrub untrusted HTML from community translations before showing it in the browser. It picks up three upstream security fixes, none of which affect this site's default configuration, and brings no code or behaviour change for users. Safe to land.


intent-alignment

  • info — Title/body say 2.18.0, but the lockfile actually resolves 2.18.1 (package-lock.json hunk @@ -7851, node_modules/sanitize-html.version: "2.18.1"). That is the better outcome: 2.18.1 fixes a stack-overflow regression introduced in 2.18.0 (upstream GHSA-c9rx-x947-fp3h) and drops the deepmerge transitive. Nothing to change; noted so the next reader isn't confused by the mismatch.

Checked: package.json:44 range ^2.17.7 already admits 2.18.1, so no manifest edit is needed; engines.node >=22.12.0 is unchanged from 2.17.7 and CI runs Node 24 (.github/workflows/ci.yml:20); the only call site src/utils/markdown.ts:12-18 uses sanitize.defaults plus target/rel on <a>, which none of the 2.18.x behaviour changes touch; lockfile integrity hash matches the npm registry; test CI job passed on this head.

No findings from: correctness, security, style, docs-currency.

Confidence: 5/5 (safe)

— hive: agent=reviewer backend=copilot model=claude-fable-5.1 copilot=1.0.88

@mergeraptor

mergeraptor Bot commented Oct 21, 2026

Copy link
Copy Markdown
Contributor Author

Edited/Blocked Notification

Renovate will not automatically rebase this PR, because it does not recognize the last commit author and assumes somebody else may have edited the PR.

You can manually request rebase by checking the rebase/retry box above.

⚠️ Warning: custom changes will be lost.

@Danathar Danathar left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Checked: the lockfile resolves sanitize-html to 2.18.1 (not the 2.18.0 in the title), and its integrity (sha512-2X5lC8f…zeDig==) matches npm view sanitize-html@2.18.1 dist.integrity. 2.18.1 is the current npm latest. This is a lock-only change, and CI passes at this head. Not checked: rendering of sanitized content on the site.

@hivecommons-hive

Copy link
Copy Markdown
Contributor

🔒 Hold-gate integrity: branch changed while hold-gated — fresh review required

This PR's branch moved while it sat hold-gated: the head recorded when the hold was applied was 0172fef492ea, but the branch now sits at 45a194529f03. The diff a reviewer saw under the hold is no longer the diff that would merge, so auto-merge is blocked and the hold label has been re-applied.

Commits not present in the hold-time snapshot (1):

  • 45a194529f03 by hivecommons-hive[bot] — chore(deps): update sanitize-html lock to 2.18.1

Authors not in the hold-time author set: hivecommons-hive[bot]

A human should review the FULL diff at the current head — a rebase or force-push renames every commit, so everything above needs eyes even if it looks familiar. Removing the hold label after that review is the fresh approval: the guard has re-pinned its snapshot to the current head, so a clean lift re-opens the merge lanes.

@hivecommons-hive hivecommons-hive Bot added hold Work is intentionally paused. sentinel-alert labels Oct 22, 2026
@hivecommons-hive

Copy link
Copy Markdown
Contributor
45a1945

⚠️ Sentinel alert — maintainer review required

Hive flagged this PR (author @mergeraptor[bot], head 45a194529f03) because it matches behaviors that can override security controls, escalate privileges or damage the codebase. This is a heuristic, not an accusation — a maintainer should confirm the change is intended before it merges.

  • sensitive_path — changes 1 sensitive path(s) (Touches a sensitive path (OWNERS, workflows, policies, hive config, security docs, …))
    • package-lock.json

Hive added the sentinel-alert label. While it is present, Hive will not approve this PR, apply LGTM/approval labels, or merge it through any auto-merge lane. Remove the label once reviewed; Hive will not re-apply it unless new commits are pushed. Tune paths and behaviors under sentinel in hive.yaml or the dashboard Security tab.

@Danathar Danathar removed sentinel-alert hold Work is intentionally paused. labels Oct 22, 2026
@Danathar
Danathar added this pull request to the merge queue Oct 23, 2026
Merged via the queue into main with commit 7adac22 Oct 23, 2026
3 checks passed
@Danathar
Danathar deleted the renovate/npm-sanitize-html-vulnerability branch October 23, 2026 18:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant