Skip to content

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

Merged
Shinigami92 merged 1 commit intonextfrom
renovate/npm-sanitize-html-vulnerability
Apr 17, 2026
Merged

chore(deps): update dependency sanitize-html to v2.17.3 [security]#3814
Shinigami92 merged 1 commit intonextfrom
renovate/npm-sanitize-html-vulnerability

Conversation

@renovate
Copy link
Copy Markdown
Contributor

@renovate renovate Bot commented Apr 16, 2026

This PR contains the following updates:

Package Change Age Confidence
sanitize-html (source) 2.17.22.17.3 age confidence

GitHub Vulnerability Alerts

CVE-2026-40186

Summary

Commit 49d0bb7 introduced a regression in sanitize-html that bypasses allowedTags enforcement for text inside nonTextTagsArray elements (textarea and option). Entity-encoded HTML inside these elements passes through the sanitizer as decoded, unescaped HTML, allowing injection of arbitrary tags including XSS payloads. This affects any application using sanitize-html that includes option or textarea in its allowedTags configuration.

Details

The vulnerable code is at packages/sanitize-html/index.js:569-573:

} else if ((options.disallowedTagsMode === 'discard' || options.disallowedTagsMode === 'completelyDiscard') && (nonTextTagsArray.indexOf(tag) !== -1)) {
  // htmlparser2 does not decode entities inside raw text elements like
  // textarea and option. The text is already properly encoded, so pass
  // it through without additional escaping to avoid double-encoding.
  result += text;
}

The comment is factually incorrect. htmlparser2 10.x does decode HTML entities inside both <textarea> and <option> elements before passing text to the ontext callback. This can be verified:

const htmlparser2 = require('htmlparser2');
const parser = new htmlparser2.Parser({
  ontext(text) { console.log(JSON.stringify(text)); }
});
parser.write('<option>&lt;script&gt;</option>');
// Outputs: "<", "script", ">"  — entities are decoded

Because the code assumes the text is "already properly encoded" and skips escapeHtml(), the decoded entities (<, >) are written directly to the output as literal HTML characters. This completely bypasses the allowedTags filter — any tag can be injected inside an allowed option or textarea element using entity encoding.

The execution flow:

  1. Attacker submits: <option>&lt;img src=x onerror=alert(1)&gt;</option>
  2. htmlparser2 parses and decodes entities → ontext receives <img src=x onerror=alert(1)>
  3. Code at line 569 checks: tag is option, which is in nonTextTagsArray → true
  4. Line 573: result += text — writes decoded text directly without escaping
  5. Output: <option><img src=x onerror=alert(1)></option><img> tag injected despite not being in allowedTags

The script and style tags are handled separately at lines 563-568 (before the vulnerable block), so the effective vulnerability applies to textarea and option, plus any custom elements added to nonTextTags by the user.

Prior to commit 49d0bb7, text in these elements fell through to the escapeHtml branch (line 574-580), which correctly re-encoded the decoded entities.

PoC

Prerequisites: Application using sanitize-html 2.17.2 with option or textarea in allowedTags.

Step 1: Basic tag injection via option

const sanitize = require('sanitize-html');
const output = sanitize(
  '<option>&lt;script&gt;alert(1)&lt;/script&gt;</option>',
  { allowedTags: ['option'] }
);
console.log(output);
// Expected (safe): <option>&lt;script&gt;alert(1)&lt;/script&gt;</option>
// Actual (vulnerable): <option><script>alert(1)</script></option>

Step 2: Element breakout with XSS event handler

const output2 = sanitize(
  '<option>&lt;/option&gt;&lt;img src=x onerror=alert(document.cookie)&gt;</option>',
  { allowedTags: ['option'] }
);
console.log(output2);
// Output: <option></option><img src=x onerror=alert(document.cookie)></option>
// The <img> tag escapes the option context and executes the onerror handler

Step 3: Textarea breakout (also vulnerable)

const output3 = sanitize(
  '<textarea>&lt;/textarea&gt;&lt;img src=x onerror=alert(1)&gt;</textarea>',
  { allowedTags: ['textarea'] }
);
console.log(output3);
// Output: <textarea></textarea><img src=x onerror=alert(1)></textarea>

Step 4: Full select/option context breakout

const output4 = sanitize(
  '<select><option>&lt;/option&gt;&lt;/select&gt;&lt;img src=x onerror=alert(1)&gt;</option></select>',
  { allowedTags: ['select', 'option'] }
);
console.log(output4);
// Output: <select><option></option></select><img src=x onerror=alert(1)></option></select>
// Breaks out of both option and select elements

All outputs verified against sanitize-html 2.17.2 with htmlparser2 10.x.

Impact

  • Complete allowedTags bypass: Any HTML tag can be injected through an allowed option or textarea element using entity encoding, defeating the core security guarantee of sanitize-html.
  • Stored XSS: Applications that sanitize user-submitted HTML and allow option or textarea tags (common in form builders, CMS platforms, rich text editors) are vulnerable to stored cross-site scripting.
  • Session hijacking: Attackers can inject event handlers (onerror, onload, etc.) to steal session cookies or authentication tokens.
  • Scope: Affects non-default configurations only — the default allowedTags does not include option or textarea. However, these tags are commonly allowed in applications that handle form-related HTML content.

Recommended Fix

Remove the vulnerable code block at lines 569-573 entirely. The escapeHtml branch (line 574) correctly handles these elements — htmlparser2 10.x decodes entities, and re-encoding with escapeHtml produces correct HTML output (entities are round-tripped, not double-encoded).

--- a/packages/sanitize-html/index.js
+++ b/packages/sanitize-html/index.js
@&#8203;@&#8203; -566,11 +566,6 @&#8203;@&#8203; function sanitizeHtml(html, options, _recursing) {
         // your concern, don't allow them. The same is essentially true for style tags
         // which have their own collection of XSS vectors.
         result += text;
-      } else if ((options.disallowedTagsMode === 'discard' || options.disallowedTagsMode === 'completelyDiscard') && (nonTextTagsArray.indexOf(tag) !== -1)) {
-        // htmlparser2 does not decode entities inside raw text elements like
-        // textarea and option. The text is already properly encoded, so pass
-        // it through without additional escaping to avoid double-encoding.
-        result += text;
       } else if (!addedText) {
         const escaped = escapeHtml(text, false);
         if (options.textFilter) {

This fix restores the pre-49d0bb7 behavior where all non-script/style text content goes through escapeHtml(), ensuring decoded entities are properly re-encoded before output.

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

Release Notes

apostrophecms/apostrophe (sanitize-html)

v2.17.3

Compare Source

Security
  • Fix vulnerability introduced in version 2.17.2 that allowed XSS attacks if the developer chose to permit option tags. There was no vulnerability when not explicitly allowing option tags.

Configuration

📅 Schedule: (UTC)

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

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

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 was generated by Mend Renovate. View the repository job log.

@renovate renovate Bot added c: security Indicates a vulnerability p: 2-high Fix main branch labels Apr 16, 2026
@renovate renovate Bot added this to the vAnytime milestone Apr 16, 2026
@renovate renovate Bot requested a review from a team as a code owner April 16, 2026 22:14
@renovate renovate Bot added p: 2-high Fix main branch c: security Indicates a vulnerability labels Apr 16, 2026
@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 16, 2026

Deploy Preview for fakerjs ready!

Name Link
🔨 Latest commit a80ea90
🔍 Latest deploy log https://app.netlify.com/projects/fakerjs/deploys/69e15f541f803d00080a8244
😎 Deploy Preview https://deploy-preview-3814.fakerjs.dev
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

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

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 16, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.89%. Comparing base (5a37b66) to head (a80ea90).
⚠️ Report is 1 commits behind head on next.

Additional details and impacted files
@@           Coverage Diff           @@
##             next    #3814   +/-   ##
=======================================
  Coverage   98.89%   98.89%           
=======================================
  Files         894      894           
  Lines        3086     3086           
  Branches      564      564           
=======================================
  Hits         3052     3052           
  Misses         30       30           
  Partials        4        4           
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@Shinigami92 Shinigami92 enabled auto-merge April 17, 2026 03:36
@Shinigami92 Shinigami92 disabled auto-merge April 17, 2026 03:36
@Shinigami92 Shinigami92 added this pull request to the merge queue Apr 17, 2026
Merged via the queue into next with commit bbabc4b Apr 17, 2026
40 of 41 checks passed
@Shinigami92 Shinigami92 deleted the renovate/npm-sanitize-html-vulnerability branch April 17, 2026 03:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

c: security Indicates a vulnerability p: 2-high Fix main branch

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant