sanitize-html allowedTags Bypass via Entity-Decoded Text in nonTextTags Elements
Date: 2026-04
Affected: npm/sanitize-html
Attack vector: xss
Category: vulnerability-disclosure
Severity: Medium
CVE: CVE-2026-40186
CWE: CWE-79
CVSS: 6.1 (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N)

## 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`: ```javascript } 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: ```javascript 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 `escapeHtm


Mitigations:

Architectural layers (MAESTRO):