---
id: CVE-2026-113654
aliases:
  - GHSA-x3q4-9hxx-gx8m
title: >-
  ApostropheCMS sanitize-html: nonTextTags discard region ends early on an
  implied close, letting <noscript> content render
summary: >-
  ApostropheCMS sanitize-html: nonTextTags discard region ends early on an
  implied close, letting <noscript> content render
severity: medium
cvss: 5.3
cwe:
  - CWE-79
vendor: sanitize-html
product: sanitize-html
ecosystem: npm
affected:
  - sanitize-html < 2.18.0
patched:
  - sanitize-html 2.18.0
published: '2026-10-20'
updated: '2026-10-20'
sourceUpdated: '2026-10-20T18:03:24Z'
source: GHSA
sourceUrl: 'https://github.com/advisories/GHSA-x3q4-9hxx-gx8m'
references:
  - url: >-
      https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-x3q4-9hxx-gx8m
  - url: >-
      https://github.com/apostrophecms/apostrophe/commit/9500b8b0e1d630d5e07fbb5fd2c25ed0943845ce
  - url: >-
      https://github.com/apostrophecms/apostrophe/commit/f84fd41cbf6c22eb8d0008fc9c25df1f7130b350
  - url: >-
      https://github.com/apostrophecms/apostrophe/releases/tag/sanitize-html@2.18.0
  - url: 'https://github.com/advisories/GHSA-x3q4-9hxx-gx8m'
tags:
  - ghsa
  - npm
  - cve.org
cvssVector: 'CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:L/VI:L/VA:L/SC:N/SI:N/SA:N'
cvssSource: cna
ingestedAt: '2026-10-20T17:12:53.600Z'
---

## Overview

## Summary

`sanitize-html` discards the *content* of every element in `nonTextTags`. It delimits that region
using htmlparser2's close events, and ignores the `isImplied` flag htmlparser2 passes it. Closing a
still-open **ancestor** from inside a `<noscript>` makes htmlparser2 unwind its stack past the
`noscript`; the synthetic implied close it emits on the way ends the discard region early, and
everything after it is treated as ordinary body content.

A browser with scripting enabled parses `<noscript>` content as **raw text**, so the same markup
renders as inert text there. The sanitizer therefore emits elements the page itself would not have.

## Reproduction

```js
const sanitizeHtml = require("sanitize-html");

sanitizeHtml('<div><noscript></div><img src="http://evil.test/t.gif"></noscript>', {
  allowedTags: ["div", "img"],
  allowedAttributes: { img: ["src"] },
  nonTextTags: ["script", "style", "textarea", "option", "noscript"],
});
// => '<div></div><img src="http://evil.test/t.gif" />'
```

Chrome, given the same input, renders **no** `<img>` — the whole payload is raw text inside the
unclosed `<noscript>`. Verified with Playwright, `srcdoc` into an iframe so the scripting flag is on.

A `<![CDATA[` variant hides the opening tag from the parser entirely and has the same effect:

```
<p>Hi</p><![CDATA[ x > <noscript>]]><a href="…">Reset your password</a><img src="…beacon.gif"></noscript>
```

htmlparser2 runs a CDATA token to `]]>`; Chrome ends a bogus comment at the first `>`, so the
`<noscript>` after it is real to the browser and invisible to the parser.

## Where the behaviour is

`index.js`:

- `:154` — `nonTextTagsArray` (the default list, `['script','style','textarea','option','xmp']`).
- `:638` — `onclosetag: function (name, isImplied)` — receives the flag.
- `:644` — `skipTextDepth--` — decrements without consulting `isImplied`.

## Suggested fix

Do not end a discard region on an implied close. htmlparser2 already distinguishes them; the flag
is passed and discarded. Something close to:

```js
onclosetag: function (name, isImplied) {
  if (skipText) {
    if (isImplied) return;   // an implied close must not end the region
    skipTextDepth--;
    …
```

## Affected packages

- `sanitize-html < 2.18.0`

## Remediation

Upgrade to a patched release:

- `sanitize-html 2.18.0`
