GCVE Workshop - 22 September 2026 (14:00-18:00), Luxembourg Before The Vulnopticon Conference - Registration

GHSA-JXWJ-J7WR-GFRW

Vulnerability from github – Published: 2026-09-03 20:07 – Updated: 2026-09-03 20:07
VLAI
Summary
ApostropheCMS: Mutation-XSS / allowedTags bypass via literal `</textarea/>` solidus close
Details

Summary

A mutation-XSS / allowedTags bypass: when textarea (or xmp) is included in allowedTags, an input containing a literal </textarea/> (a solidus right after the RCDATA end-tag name) lets non-allowed markup such as <img src=x onerror=…> pass through sanitizeHtml() live and unescaped, even though img/onerror are not in the allowlist. A spec-compliant browser executes the surviving handler — XSS. This is a literal-solidus variant that bypasses the two most recent fixes in this code area (CVE-2026-40186, CVE-2026-44990), both already applied in 2.17.5. The default configuration is not affected.

Details

sanitize-html emits the text content of HTML raw-text elements (textarea, xmp) without escaping. Two things combine: - Parser differential: on input, htmlparser2 does NOT recognize </textarea/> (solidus after the RCDATA end-tag name) as a close tag; it emits </textarea/><img …> as a single raw-text node. - Unescaped passthrough: the ontext handler (index.js ~575-583) appends textarea/xmp content with result += text (no escapeHtml), assuming it is "already properly encoded" — true for entity-decoded content (what CVE-2026-40186 fixed) but false for this mis-tokenized literal close tag. A spec browser treats </textarea/> as a valid textarea close, so the following <img onerror> is parsed as a live element. The recent fixes addressed entity-encoding (CVE-2026-40186) and the xmp default (CVE-2026-44990); neither covers the literal-solidus mis-tokenization, so the raw passthrough still leaks.

PoC

// npm i sanitize-html@2.17.5 parse5 && node poc.js
const sanitizeHtml = require('sanitize-html');
const input = '<textarea></textarea/><img src=x onerror="alert(document.domain)">';
const opts  = { allowedTags: sanitizeHtml.defaults.allowedTags.concat(['textarea']) };  // img NOT allowed
console.log(sanitizeHtml(input, opts));
// => <textarea></textarea/><img src=x onerror="alert(document.domain)"></textarea>
//    the <img onerror> survives live and unescaped

console.log(sanitizeHtml(input));   // default config (no textarea allowed) => ""  (safe)

Re-parsing the sanitized OUTPUT with parse5 (the WHATWG HTML parser browsers/jsdom use) yields a live <img src=x onerror=alert(document.domain)> at body level (it escaped the textarea RCDATA, not inert text) → the onerror fires in a browser. Confirmed on 2.17.5 (Node v24). A canonical poc.js is attached. image

Impact

Cross-site scripting (CWE-79). Requires textarea (or xmp) in allowedTags — a benign-looking, common addition in form builders, CMS, and rich-text editors. Adding a harmless tag that then enables XSS via non-allowed img/onerror breaks the sanitizer's core contract; the maintainers have fixed this class before (e.g. GHSA-9mrh). An attacker who can submit content rendered through such a configuration achieves stored/reflected XSS (cookie theft, session hijack). Severity Medium (default config is safe; user interaction to view the page). Suggested fix: route textarea/xmp content through escapeHtml instead of the raw passthrough, and/or fix the htmlparser2 </tag/> RCDATA end-tag tokenization to match the WHATWG spec.

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 2.17.5"
      },
      "package": {
        "ecosystem": "npm",
        "name": "sanitize-html"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "2.17.6"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-63670"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-79"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-09-03T20:07:16Z",
    "nvd_published_at": "2026-08-17T20:16:45Z",
    "severity": "MODERATE"
  },
  "details": "### Summary\nA mutation-XSS / allowedTags bypass: when `textarea` (or `xmp`) is included in `allowedTags`, an input containing a literal `\u003c/textarea/\u003e` (a solidus right after the RCDATA end-tag name) lets non-allowed markup such as `\u003cimg src=x onerror=\u2026\u003e` pass through `sanitizeHtml()` **live and unescaped**, even though `img`/`onerror` are not in the allowlist. A spec-compliant browser executes the surviving handler \u2014 XSS. This is a literal-solidus variant that bypasses the two most recent fixes in this code area (CVE-2026-40186, CVE-2026-44990), both already applied in 2.17.5. The default configuration is not affected.\n\n### Details\n`sanitize-html` emits the text content of HTML raw-text elements (`textarea`, `xmp`) without escaping. Two things combine:\n- **Parser differential:** on input, htmlparser2 does NOT recognize `\u003c/textarea/\u003e` (solidus after the RCDATA\n  end-tag name) as a close tag; it emits `\u003c/textarea/\u003e\u003cimg \u2026\u003e` as a single raw-text node.\n- **Unescaped passthrough:** the `ontext` handler (`index.js` ~575-583) appends `textarea`/`xmp` content with\n`result += text` (no `escapeHtml`), assuming it is \"already properly encoded\" \u2014 true for entity-decoded\n  content (what CVE-2026-40186 fixed) but false for this mis-tokenized literal close tag.\nA spec browser treats `\u003c/textarea/\u003e` as a valid `textarea` close, so the following `\u003cimg onerror\u003e` is parsed as a live element. The recent fixes addressed entity-encoding (CVE-2026-40186) and the `xmp` default (CVE-2026-44990); neither covers the literal-solidus mis-tokenization, so the raw passthrough still leaks.\n\n### PoC\n```js\n// npm i sanitize-html@2.17.5 parse5 \u0026\u0026 node poc.js\nconst sanitizeHtml = require(\u0027sanitize-html\u0027);\nconst input = \u0027\u003ctextarea\u003e\u003c/textarea/\u003e\u003cimg src=x onerror=\"alert(document.domain)\"\u003e\u0027;\nconst opts  = { allowedTags: sanitizeHtml.defaults.allowedTags.concat([\u0027textarea\u0027]) };  // img NOT allowed\nconsole.log(sanitizeHtml(input, opts));\n// =\u003e \u003ctextarea\u003e\u003c/textarea/\u003e\u003cimg src=x onerror=\"alert(document.domain)\"\u003e\u003c/textarea\u003e\n//    the \u003cimg onerror\u003e survives live and unescaped\n\nconsole.log(sanitizeHtml(input));   // default config (no textarea allowed) =\u003e \"\"  (safe)\n```\nRe-parsing the sanitized OUTPUT with **parse5** (the WHATWG HTML parser browsers/jsdom use) yields a **live\n`\u003cimg src=x onerror=alert(document.domain)\u003e`** at `body` level (it escaped the textarea RCDATA, not inert text) \u2192 the `onerror` fires in a browser. Confirmed on 2.17.5 (Node v24). A canonical `poc.js` is attached.\n\u003cimg width=\"650\" height=\"118\" alt=\"image\" src=\"https://github.com/user-attachments/assets/d63bb5b7-ba3e-4b0e-a821-86a453ea0352\" /\u003e\n\n### Impact\nCross-site scripting (CWE-79). Requires `textarea` (or `xmp`) in `allowedTags` \u2014 a benign-looking, common addition in form builders, CMS, and rich-text editors. Adding a harmless tag that then enables XSS via *non-allowed* `img`/`onerror` breaks the sanitizer\u0027s core contract; the maintainers have fixed this class before (e.g. GHSA-9mrh). An attacker who can submit content rendered through such a configuration achieves stored/reflected XSS (cookie theft, session hijack). Severity Medium (default config is safe; user interaction to view the page). **Suggested fix:** route `textarea`/`xmp` content through `escapeHtml` instead of the raw passthrough, and/or fix the htmlparser2 `\u003c/tag/\u003e` RCDATA end-tag tokenization to match the WHATWG spec.",
  "id": "GHSA-jxwj-j7wr-gfrw",
  "modified": "2026-09-03T20:07:16Z",
  "published": "2026-09-03T20:07:16Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-jxwj-j7wr-gfrw"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-63670"
    },
    {
      "type": "WEB",
      "url": "https://github.com/apostrophecms/apostrophe/pull/5501"
    },
    {
      "type": "WEB",
      "url": "https://github.com/apostrophecms/apostrophe/commit/eae1fb2b72ec5d1c27d0977509c6482a0408f725"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/apostrophecms/apostrophe"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "ApostropheCMS: Mutation-XSS / allowedTags bypass via literal `\u003c/textarea/\u003e` solidus close"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Loading…