ApostropheCMS (Node.js CMS) is vulnerable to an XSS bypass in its HTML sanitization dependency, sanitize-html. The issue is caused by a regression in sanitize-html versions starting from 2.17.1 (introduced in commit 49d0bb7). ApostropheCMS versions from 4.28.0 are affected because they depend on the vulnerable sanitize-html version. The flaw allows attackers to inject arbitrary HTML tags (and XSS payloads) through entity-encoded content placed inside certain elements—specifically <option> and <textarea>—when the application configuration permits those tags via sanitize-html’s allowedTags settings.
The root cause is incorrect handling of entity-decoded text for “nonTextTagsArray” elements in sanitize-html. In packages/sanitize-html/index.js around lines 569–573, the sanitizer contains logic that, under certain disallowedTagsMode settings, directly appends the parser-provided text to the output (result += text) without escaping. The vulnerable code path is:
- sanitizeHtml(html, options, _recursing)
- Conditional branch checks: (options.disallowedTagsMode === 'discard' || 'completelyDiscard') AND (nonTextTagsArray.indexOf(tag) !== -1)
- Then it assumes htmlparser2 does not decode entities inside raw text elements like textarea and option, and therefore skips escaping to avoid double-encoding.
However, the advisory explains that this assumption is wrong for htmlparser2 10.x: htmlparser2 decodes HTML entities inside both <textarea> and <option> before invoking the ontext callback. As a result, entity-encoded payloads such as <img ...> are decoded by the parser into literal characters (<img ...>) and then written directly to the sanitizer output via result += text, completely bypassing the allowedTags enforcement.
The exploitation mechanism is configuration-dependent: it requires a non-default sanitize-html configuration where developers include option or textarea in allowedTags (common in form builders/CMS platforms). An attacker can submit content that places entity-encoded HTML inside an allowed <option> or <textarea> element. During sanitization, the entities are decoded by htmlparser2 and then emitted unescaped, enabling injection of arbitrary tags and XSS payloads.
Impact: This is an XSS injection vulnerability (CWE-79) with a CVSS score of 6.1 (Medium) and vector CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N. In practical terms, an attacker can cause script execution in the context of users who view the sanitized content, leading to client-side compromise (confidentiality and integrity impact are marked as low). The blast radius is limited to applications that use sanitize-html and configure allowedTags to include option and/or textarea (and are on vulnerable versions). Real-world risk is elevated in CMS/form-builder scenarios where these tags are commonly allowed.
Mitigation: Upgrade to the fixed versions. The sanitize-html regression is fixed in version 2.17.2, and ApostropheCMS is fixed in version 4.29.0. Specifically:
- If you use sanitize-html directly: update to sanitize-html >= 2.17.2.
- If you use ApostropheCMS: upgrade to ApostropheCMS 4.29.0 (fixes the dependency).
The patch narrows the “raw text / no-escape” behavior to tags that htmlparser2 truly treats as raw text elements without entity decoding (the commit shows the change from handling nonTextTagsArray broadly to explicitly checking tag === 'textarea' || tag === 'xmp'). It also clarifies that other nonTextTags like <option> are decoded by htmlparser2 and therefore must be escaped like normal text to prevent entity-encoded XSS payloads.
Detection guidance (based on the described behavior): review sanitize-html configurations for allowedTags that include option and/or textarea, and ensure the runtime dependency versions are at or above the fixed releases. If you cannot upgrade immediately, treat any user-controlled content rendered inside allowed <option> or <textarea> as high risk and avoid allowing those tags in allowedTags unless absolutely necessary.
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N
CWE-79