Skip to content

Bump sanitize-html from 2.17.4 to 2.17.6 - #1467

Open
dependabot[bot] wants to merge 3 commits into
mainfrom
dependabot/npm_and_yarn/sanitize-html-2.17.6
Open

Bump sanitize-html from 2.17.4 to 2.17.6#1467
dependabot[bot] wants to merge 3 commits into
mainfrom
dependabot/npm_and_yarn/sanitize-html-2.17.6

Conversation

@dependabot

@dependabot dependabot Bot commented on behalf of github Aug 1, 2026

Copy link
Copy Markdown
Contributor

Bumps sanitize-html from 2.17.4 to 2.17.6.

Changelog

Sourced from sanitize-html's changelog.

2.17.6 (2026-07-10)

Fixes

  • Allow transformTags to emit text when textFilter is set, even if the tag is initially empty. This is consistent with the documentation. Thanks to spokodev for the fix.

Security

  • Fixed an XSS/allowlist bypass in which the contents of a raw-text element (textarea or xmp) nested inside an svg or math root were re-emitted without HTML-escaping. sanitize-html treated that content as inert raw text because htmlparser2 10.x classified raw-text elements by tag name and ignored the namespace, but a real HTML5 parser treats textarea/xmp as ordinary foreign elements inside SVG/MathML and re-parses their contents as live markup. As a result, markup and event-handler attributes that the allowlist never permitted (for example <svg><textarea><img src=x onerror=alert(1)>) could survive sanitization and execute in the browser. This is now fixed on two fronts: htmlparser2 was upgraded to 12.x, which is namespace-aware and parses textarea/xmp inside SVG/MathML as ordinary elements, so their non-allowlisted children (such as the injected img) are dropped by the allowlist instead of being preserved as raw text; and any raw-text content sanitize-html still emits for these tags (at HTML integration points such as foreignObject/mtext, or outside foreign content) is always HTML-escaped. The default configuration is not affected; the precondition is an allowedTags that includes svg or math together with textarea or xmp. Thanks to khoadb175 for responsibly disclosing the vulnerability.
  • Fixed a mutation-XSS / allowedTags bypass affecting configurations that allow the textarea or xmp raw-text tags. htmlparser2 10.x did not recognize an end tag with a trailing solidus (e.g. </textarea/>) as closing the element, so it kept the following markup as raw text, but a spec-compliant browser treats </textarea/> as a valid close and parses that markup as a live element. Because raw-text content was re-emitted without escaping, a payload such as <textarea></textarea/><img src=x onerror=...> could smuggle non-allowlisted, executable markup through the sanitizer. The default configuration was not affected. This is now defended at two layers: htmlparser2 was upgraded to 12.x, whose tokenizer closes these end tags correctly, and the raw text sanitize-html emits for these tags is always escaped so no < can reopen a tag when the output is re-parsed (textarea, an RCDATA element whose entities htmlparser2 decodes, is escaped like normal text, while xmp, a raw-text element, has only its angle brackets escaped to avoid double-encoding already-encoded entities). Because htmlparser2 is ESM-only from version 11 onward, sanitize-html now requires Node.js >=22.12.0 (the first 22.x release in which require() of an ES module is available unflagged). Thanks to bibu123456 for reporting the vulnerability and Kayiz-PT for coordinating the disclosure (GHSA-jxwj-j7wr-gfrw).

2.17.5 (2026-06-10)

Security

  • Added a number of new attributes to be protected against unsafe URLs, e.g. javascript: and similar. None of these are used in the default configuration of sanitize-html or apostrophe or likely to be used there, and some attributes, like an action for a form, are inherently unsafe to allow if XSS protection is your goal. Nevertheless it makes sense to block certain URL types where they are not appropriate. Some attributes are not supported at all by modern browsers but are included for completeness. Thanks to crattack for reporting the vulnerability.
  • Address a potential vulnerability when nonTextTags is configured in a nonstandard way. While it is never a good idea to remove known non-text tags from the standard list e.g. script, styles, etc., this change ensures that doing so does not result in nested tags being passed through without sanitization when they are not expressly allowed. (ApostropheCMS would never trigger this situation.) Thanks to Dipanshu singh for pointing out the issue and contributing the fix.
Commits

Dependabot compatibility score

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting @dependabot rebase.


Dependabot commands and options

You can trigger Dependabot actions by commenting on this PR:

  • @dependabot rebase will rebase this PR
  • @dependabot recreate will recreate this PR, overwriting any edits that have been made to it
  • @dependabot show <dependency name> ignore conditions will show all of the ignore conditions of the specified dependency
  • @dependabot ignore this major version will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this minor version will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
  • @dependabot ignore this dependency will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
    You can disable automated security fix PRs for this repo from the Security Alerts page.

Bumps [sanitize-html](https://github.com/apostrophecms/apostrophe/tree/HEAD/packages/sanitize-html) from 2.17.4 to 2.17.6.
- [Changelog](https://github.com/apostrophecms/apostrophe/blob/main/packages/sanitize-html/CHANGELOG.md)
- [Commits](https://github.com/apostrophecms/apostrophe/commits/HEAD/packages/sanitize-html)

---
updated-dependencies:
- dependency-name: sanitize-html
  dependency-version: 2.17.6
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
@dependabot dependabot Bot added dependencies Pull requests that update a dependency file javascript Pull requests that update javascript code labels Aug 1, 2026
@netlify

netlify Bot commented Aug 1, 2026

Copy link
Copy Markdown

Deploy Preview for bettervoting ready!

Name Link
🔨 Latest commit 6fd9b09
🔍 Latest deploy log https://app.netlify.com/projects/bettervoting/deploys/6a70eb55d183570008b4789d
😎 Deploy Preview https://deploy-preview-1467--bettervoting.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@ArendPeter

Copy link
Copy Markdown
Member

This is causing all our tests to fail

    SyntaxError: Cannot use import statement outside a module

      3 | import ServiceLocator from './ServiceLocator';
      4 | import { Election } from '@equal-vote/star-vote-shared/domain_model/Election';
    > 5 | import sanitizeHtml from 'sanitize-html';
        | ^
      6 |
      7 | export function assertNotNull<Type>(data:Type | null, message:string = 'unexpected null'):Type {
      8 |     if (data == null){

It's already been reported at apostrophecms/apostrophe#5526

@ArendPeter

Copy link
Copy Markdown
Member

I considered a lot of approaches with my AI. Ultimately we decided to mock out sanitize-html since it's new build configuration wasn't compatible with jest. That's okay since we weren't testing sanitize-html anyway, and we still have the E2E tests to catch build failures. More details below:


Backend tests failing after sanitize-html 2.17.4 → 2.17.6 bump

Root cause

sanitize-html 2.17.6 is a genuine security patch (XSS/mutation-XSS fix, GHSA-jxwj-j7wr-gfrw). As part of the fix it bumps its htmlparser2 dependency to v12, which dropped its CommonJS build entirely — htmlparser2@12 is pure ESM with no require export condition.

  • Node itself is unaffected: Node 22.12+/24 (what we run) added native support for require() to synchronously load an ES module, so the app runs fine in dev and prod.
  • Jest breaks: Jest doesn't use Node's real module loader. It has its own reimplementation (jest-runtime) that reads each file's source and stitches it into a function(module, exports, require, ...){ <source> } wrapper, then evaluates that via vm.Script. When <source> is raw ESM (import { Parser } from "./Parser.js"), that's a syntax error inside a function body — Jest never calls Node's real require(), so it can't inherit Node's require(esm) capability. This is structural to how Jest sandboxes modules (coverage instrumentation, per-test isolation, mocking), not a config oversight.

Upstream tracked this exact Jest/ESM friction at apostrophecms/apostrophe#5526 and closed it with no fix — their changelog explicitly states they now require Node ≥22.12 for native require(esm) and have no CJS build planned. Waiting for a future release will not resolve this.

Options considered

Mock sanitize-html in Jest (chosen) Babel-transpile the ESM chain for Jest
How packages/backend/src/__mocks__/sanitize-html.js — Jest's manual-mock convention for node_modules packages applies it automatically, no config changes @babel/preset-env + transformIgnorePatterns transpile htmlparser2/domhandler/domutils/dom-serializer/domelementtype/entities to CJS before Jest wraps them
Exercises real sanitize-html logic under test No Yes
New toolchain / deps None @babel/preset-env, @babel/plugin-transform-modules-commonjs, new babel.config.js
Maintenance burden Only if sanitize-html's usage in our code changes Must keep the transformIgnorePatterns package allowlist in sync if htmlparser2's dependency chain changes again
Test suite speed ~9–15s ~21–27s (measured, cold and warm cache)
Fidelity to prod N/A (stubbed) High, but not identical — prod uses Node's native require(esm) with zero transformation; this runs Babel-transpiled CJS of the same source

Both were implemented and verified (all 22 suites / 149 tests passing under each). We went with the mock:

  • No unit test in the repo asserts on sanitize-html's actual output (it's used only for stripping/allow-listing HTML in election metadata, emails, and markdown rendering — verified via grep), so the extra fidelity of the Babel route isn't buying unit-test coverage, just cost (new toolchain, ~2x slower suite, an extra config surface to maintain).
  • Prod fidelity is already covered by the Playwright E2E suite, which exercises the real built app (real Node runtime, real sanitize-html, real htmlparser2@12). The Babel route would have been redundant fidelity at the unit-test layer, not new coverage.

⚠️ Do not "fix" this by downgrading htmlparser2

An earlier iteration of this fix pinned htmlparser2 back to the CJS-compatible v10 line via package.json overrides. This is wrong — it silently reverts part of a real XSS fix repo-wide. It happened to be harmless today only because our 3 current sanitize-html call sites (Util.ts, EmailTemplates.ts — both allowedTags: []; formatMarkdown.ts — uses sanitizeHtml.defaults.allowedTags, which doesn't include svg/math/textarea/xmp) don't hit the vulnerable precondition. That's a fragile guarantee that breaks silently the moment someone's allowedTags config changes. Flagged explicitly in CLAUDE.md so it doesn't get reintroduced.

Files changed

  • packages/backend/src/__mocks__/sanitize-html.js — new passthrough mock, placed alongside the repo's existing manual-mock convention (__mocks__/ServiceLocator.ts) and picked up automatically by Jest — no jest.config.js changes needed
  • CLAUDE.md — documented the root cause and the "don't downgrade htmlparser2" warning

(That stray jest.config.js diagnostic in your IDE is just a pre-existing lint hint unrelated to this change — jest.config.js is back to its original untouched state, not something I introduced.)

@ArendPeter
ArendPeter requested a review from jacksonloper August 3, 2026 21:46
@jacksonloper

Copy link
Copy Markdown
Collaborator

@ArendPeter another solution would be to update jest (we are running jest from september 2023 right now, could be reasonable to go up to 30.4). I think the problem is that the latest htmlparser is packaged differently, and the old jest gets confused by that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file javascript Pull requests that update javascript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants