Repository navigation
chore(deps): update dependency sanitize-html to v2.18.0 [security] - #957
Conversation
0172fef
|
There was a problem hiding this comment.
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:7854pinssanitize-htmlto2.18.0. UpstreamCHANGELOG.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 throwRangeError: Maximum call stack size exceededwith the default options … Only 2.18.0 was affected." This site runs the sanitizer withsanitize.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 whensanitize-html@2.18.1is already on npm defeats the purpose of a[security]bump. Fix: update the lock to 2.18.1 (the^2.17.7range inpackage.json:44already permits it). Note 2.18.1 also drops thedeepmergedependency, 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>
There was a problem hiding this comment.
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 resolves2.18.1(package-lock.jsonhunk@@ -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 thedeepmergetransitive. 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
Edited/Blocked NotificationRenovate 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. |
Danathar
left a comment
There was a problem hiding this comment.
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.
🔒 Hold-gate integrity: branch changed while hold-gated — fresh review requiredThis PR's branch moved while it sat hold-gated: the head recorded when the hold was applied was Commits not present in the hold-time snapshot (1):
Authors not in the hold-time author set: 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. |
45a1945
|
This PR contains the following updates:
2.17.7→2.18.0Warning
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 theirattributeNametargets a URL sink likehref. However, this check is bypassed when the animation element uses a namespace prefix (e.g.,svg:animateinstead ofanimate).Root Cause
The
animatesUrlAttributefunction (index.js, line ~832) checkssvgAnimationTags.indexOf(name.toLowerCase()), wheresvgAnimationTagscontains bare local names:["animate", "animatecolor", "animatemotion", "animatetransform", "set"]. htmlparser2 passes namespace-prefixed tag names verbatim, sosvg:animatedoes not matchanimatein the array, causing the function to returnfalseand skip the SMIL XSS protection.Ironically, the function already handles namespace prefixes correctly for the
attributeNametarget 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
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 thehrefattribute 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:
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:3.1/AV:N/AC:H/PR:L/UI:R/S:U/C:L/I:L/A:NReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
ApostropheCMS: Incomplete CVE-2026-53606:
meta http-equiv=refreshcontentstill shipsjavascript:/data:URLsCVE-2026-113661 / GHSA-cv27-6wvh-8x7j
More information
Details
Summary
CVE-2026-53606 expanded
allowedSchemesAppliedToAttributesso named URL attributes (action,formaction,data,poster,background, laterping,xlink:href, …) go throughnaughtyHref(). A URL nested insidemetacontent(http-equiv=refresh) is not an attribute name on that list, so the embedded destination is never scheme-checked.Default
allowedTagsstill omitmeta. Same non-default integrator config as 53606: allowmetaplushttp-equiv/content.On 2.17.7:
data:text/html,…and arbitraryhttps:destinations also survive.formaction="javascript:…"(53606) is stripped.Please Request CVE. Credit: Finder + Reporter (
adrbogacz).Details
naughtyHref()only runs when the attribute name is inallowedSchemesAppliedToAttributes(index.js~385). Refresh URLs live in acontentvalue of the form0;url=…, so the gate never fires. Addingcontentto that list wholesale would break ordinary metacontent(description, viewport).Fix: when
http-equivisrefresh(case-insensitive), parse the; url=destination and runnaughtyHref('meta', url). Dropcontent(or the tag) if the URL is rejected. Leave othermetacontentvalues alone.PoC
Local
sanitize-html@2.17.7. Output inspection only:Expected: refresh
javascript:/data:remain; 53606formactionis removed. Do not attach a browser hang or stored-XSS page.Impact
An integrator who allowed
meta+contentexpectingallowedSchemesto apply (the 53606 configuration pattern) still emits a refresh tojavascript:,data:, or an attackerhttps:URL. Stored XSS / open redirect for viewers of that HTML. Default config is not affected.Severity
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:NReferences
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-htmldiscards the content of every element innonTextTags. It delimits that regionusing htmlparser2's close events, and ignores the
isImpliedflag htmlparser2 passes it. Closing astill-open ancestor from inside a
<noscript>makes htmlparser2 unwind its stack past thenoscript; the synthetic implied close it emits on the way ends the discard region early, andeverything after it is treated as ordinary body content.
A browser with scripting enabled parses
<noscript>content as raw text, so the same markuprenders as inert text there. The sanitizer therefore emits elements the page itself would not have.
Reproduction
Chrome, given the same input, renders no
<img>— the whole payload is raw text inside theunclosed
<noscript>. Verified with Playwright,srcdocinto an iframe so the scripting flag is on.A
<![CDATA[variant hides the opening tag from the parser entirely and has the same effect: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 consultingisImplied.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:
Severity
CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:NReferences
This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).
Release Notes
apostrophecms/apostrophe (sanitize-html)
v2.18.0Compare Source
Adds
loggeroption: pass any console-shaped object, withdebug,info,warnanderrormethods, 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
allowedSchemesByTagis now applied tosrcsetandimagesrcsetURLs. Previously the per-tag lookup used the attribute name instead of the tag name, so these attributes always fell back to the globalallowedSchemesand ignored a tag-specific scheme allowlist. Thanks tospokodev for the fix.
sanitize-htmlbegan escaping any markup preserved inside a disallowed iframe tag, which was a changein behavior due to an upstream change in
htmlparser2. This fix ensures such "fallback markup" is preserved without escaping, but alsofully sanitized according to the same rules as the original input. Thanks to sumitjhacodes for
the fix.
Security
When
metawas allowed together with itshttp-equivandcontentattributes, the destination URL of a<meta http-equiv="refresh" content="0;url=...">was never checked againstallowedSchemes, because it is embedded incontentrather than being an attribute of its own. Sojavascript:,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 ofurl=, and checked againstallowedSchemes(orallowedSchemesByTag.meta). If it is rejected, or the content cannot be parsed as a refresh, thecontentattribute is removed.contenton othermetaelements is unchanged. The default configuration does not allowmetaand was not affected (CWE-79, CWE-601, GHSA-cv27-6wvh-8x7j).Thanks to adrbogacz for reporting the vulnerability.
When
noscriptis listed innonTextTags, 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 othernonTextTagssuch 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 ashrefcompared the full tag name, so a namespace-prefixed spelling likesvg:animatewas not recognized when such tags were allowed (for example withallowedTags: false). In XML serializations such as XHTML or standalone SVG, the prefixed element is a real animation element and could retarget a link to ajavascript:URL after sanitization. The element andattributeNameare 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)
🚦 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.
This PR has been generated by Mend Renovate CLI.