Skip to content

Commit 59ecf93

Browse files
Auto-update jsrepository: dompurify | GHSA-c2j3-45gr-mqc4 (#602)
Updated libraries (1): dompurify Updated vulnerability IDs (1): GHSA-c2j3-45gr-mqc4 Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent de0a676 commit 59ecf93

9 files changed

Lines changed: 187 additions & 0 deletions

‎repository/jsrepository-master.json‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5516,6 +5516,31 @@
55165516
"info": [
55175517
"https://github.com/cure53/DOMPurify/security/advisories/GHSA-cmwh-pvxp-8882"
55185518
]
5519+
},
5520+
{
5521+
"ranges": [
5522+
{
5523+
"atOrAbove": "0",
5524+
"below": "3.4.12"
5525+
}
5526+
],
5527+
"summary": "DOMPurify: `CUSTOM_ELEMENT_HANDLING` bypasses `afterSanitizeElements` for allowed custom elements.",
5528+
"details": "## Summary\n\nThere is a possible hook-policy inconsistency in DOMPurify 3.4.11 involving `CUSTOM_ELEMENT_HANDLING`.\n\nWhen a custom element is allowed via `CUSTOM_ELEMENT_HANDLING.tagNameCheck`, it appears that the element does not go through `afterSanitizeElements` in the same way as a normal element. As a result, an application that relies on `afterSanitizeElements` as a security policy layer to strip sensitive attributes from all elements may see those attributes removed from normal elements but preserved on allowed custom elements.\n\nThis does not appear to be a direct DOMPurify XSS or a case where DOMPurify directly allows executable payloads. The preserved value is still inert at sanitize time. The issue becomes relevant when the allowed custom element later re-injects that attribute value into an HTML sink such as `innerHTML`, creating a second-order XSS gadget.\n\n## Details\n\nThe issue appears to originate from the control flow in `src/purify.ts`: line 1672~1691\n\n```tsx\nconst _sanitizeDisallowedNode = function (\n currentNode: any,\n tagName: string\n ): boolean {\n /* Check if we have a custom element to handle */\n if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {\n if (\n CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp &&\n regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)\n ) {\n return false;\n }\n\n if (\n CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function &&\n CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)\n ) {\n return false;\n }\n }\n```\n\n`CUSTOM_ELEMENT_HANDLING` is parsed from user configuration at `src/purify.ts`: line 741~748\n\n```tsx\nconst customElementHandling =\n objectHasOwnProperty(cfg, 'CUSTOM_ELEMENT_HANDLING') &&\n cfg.CUSTOM_ELEMENT_HANDLING &&\n typeof cfg.CUSTOM_ELEMENT_HANDLING === 'object'\n ? clone(cfg.CUSTOM_ELEMENT_HANDLING)\n : create(null);\n\n CUSTOM_ELEMENT_HANDLING = create(null);\n```\n\nIn particular, `tagNameCheck`, `attributeNameCheck`, and `allowCustomizedBuiltInElements` are copied into the internal `CUSTOM_ELEMENT_HANDLING` object there.\n\nDuring element sanitization, `_sanitizeElements()` checks whether a node is forbidden or not allowlisted at `src/purify.ts`: line 1805~1814\n\n```tsx\n/* Remove element if anything forbids its presence */\n if (\n FORBID_TAGS[tagName] ||\n (!(\n EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function &&\n EXTRA_ELEMENT_HANDLING.tagCheck(tagName)\n ) &&\n !ALLOWED_TAGS[tagName])\n ) {\n return _sanitizeDisallowedNode(currentNode, tagName);\n }\n```\n\nIf so, it immediately delegates to `_sanitizeDisallowedNode(currentNode, tagName)` and returns its boolean result.\n\nInside `_sanitizeDisallowedNode()`, the custom-element-specific allow path is implemented at `src/purify.ts`: line 1672~1692\n\n```tsx\nconst _sanitizeDisallowedNode = function (\n currentNode: any,\n tagName: string\n ): boolean {\n /* Check if we have a custom element to handle */\n if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) {\n if (\n CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp &&\n regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)\n ) {\n return false;\n }\n\n if (\n CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function &&\n CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)\n ) {\n return false;\n }\n }\n```\n\nIf the node is treated as a basic custom element and `CUSTOM_ELEMENT_HANDLING.tagNameCheck` matches, the function returns `false` immediately at line 1682 or 1689, meaning “do not remove this node”.\n\nThat early `return false` is significant because control returns directly to `_sanitizeElements()` via the `return _sanitizeDisallowedNode(...)` at line 1813. As a result, the later logic in `_sanitizeElements()` is skipped for that custom element instance, including:\n\n- the namespace validation at `src/purify.ts`: line 1816~1826\n\n```tsx\n* Check whether element has a valid namespace.\n Realm-safe check (GHSA-hpcv-96wg-7vj8): use the cached Node.prototype\n nodeType getter rather than `instanceof Element`, which is realm-\n bound and short-circuits to false for any node minted in a different\n realm — letting a foreign-realm element with a forbidden namespace\n slip past the namespace check entirely. */\n const nt = getNodeType ? getNodeType(currentNode) : currentNode.nodeType;\n if (nt === NODE_TYPE.element && !_checkValidNamespace(currentNode)) {\n _forceRemove(currentNode);\n return true;\n }\n```\n\n- the fallback-tag mXSS check at `src/purify.ts`: line 1828~1837\n\n```tsx\n/* Make sure that older browsers don't get fallback-tag mXSS */\n if (\n (tagName === 'noscript' ||\n tagName === 'noembed' ||\n tagName === 'noframes') &&\n regExpTest(EXPRESSIONS.FALLBACK_TAG_CLOSE, currentNode.innerHTML)\n ) {\n _forceRemove(currentNode);\n return true;\n }\n```\n\n- most importantly for this report, the `afterSanitizeElements` hook dispatch at `src/purify.ts`: line 1850~1851.\n\n```tsx\n /* Execute a hook if present */\n _executeHooks(hooks.afterSanitizeElements, currentNode, null);\n```\n\nIn other words, a normal allowlisted element continues through `_sanitizeElements()` and reaches `hooks.afterSanitizeElements`, but a disallowed-by-default element that is revived by the `CUSTOM_ELEMENT_HANDLING.tagNameCheck` path does not. This creates a policy inconsistency: an application that relies on `afterSanitizeElements` to remove an attribute from all elements will observe that the policy is applied to normal elements but not to custom elements allowed through `CUSTOM_ELEMENT_HANDLING`.\n\nIn the PoC, the application hook removes `data-bio` from ordinary elements, but the same attribute remains on `<x-bio>` because the custom-element keep path bypasses `afterSanitizeElements`. The attribute itself is inert at sanitize time and DOMPurify is not directly allowing executable SVG/HTML through. The security impact appears when the application-defined custom element later reads the preserved `data-bio` value in `connectedCallback()` and writes it to `innerHTML`, turning the preserved attribute into a second-order XSS gadget.\n\n## PoC\n\nReproduced on DOMPurify 3.4.11.\n\n### Steps\n\n1. Save the following HTML to a file, for example `poc.html`.\n2. Open it in a browser.\n3. Observe that the `div` control loses `data-bio`, while the allowed custom element keeps it.\n4. Observe that after `connectedCallback()` runs, the candidate payload is reinserted into the DOM and executes through the custom element’s own sink.\n\n### HTML PoC\n\n```html\n<!DOCTYPE html>\n<html>\n<head>\n <meta charset=\"UTF-8\">\n <script src=\"https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.4.11/purify.min.js\"></script>\n</head>\n<body>\n<pre id=\"result\"></pre>\n\n<script>\nwindow.__controlFired = false;\nwindow.__candidateFired = false;\n\ncustomElements.define(\"x-bio\", class extends HTMLElement {\n connectedCallback() {\n const bio = this.getAttribute(\"data-bio\");\n if (bio) this.innerHTML = bio;\n }\n});\n\nDOMPurify.addHook(\"afterSanitizeElements\", node => {\n if (node.hasAttribute && node.hasAttribute(\"data-bio\")) {\n node.removeAttribute(\"data-bio\");\n }\n});\n\nconst config = {\n CUSTOM_ELEMENT_HANDLING: {\n tagNameCheck: /^x-/\n }\n};\n\nconst controlInput =\n '<div data-bio=\"&lt;img src=x onerror=window.__controlFired=true&gt;\"></div>';\n\nconst candidateInput =\n '<x-bio data-bio=\"&lt;img src=x onerror=window.__candidateFired=true&gt;\"></x-bio>';\n\nconst cleanControl = DOMPurify.sanitize(controlInput, config);\nconst cleanCandidate = DOMPurify.sanitize(candidateInput, config);\n\nconst container = document.createElement(\"div\");\ncontainer.innerHTML = cleanCandidate;\ndocument.body.appendChild(container);\n\nsetTimeout(() => {\n document.getElementById(\"result\").textContent =\n \"This is not direct DOMPurify XSS.\\n\" +\n \"The payload becomes executable only after x-bio writes data-bio into innerHTML.\\n\\n\" +\n \"control: \" + cleanControl + \"\\n\" +\n \"candidate: \" + cleanCandidate + \"\\n\" +\n \"after connectedCallback: \" + container.innerHTML + \"\\n\" +\n \"control fired: \" + window.__controlFired + \"\\n\" +\n \"candidate fired: \" + window.__candidateFired;\n}, 100);\n</script>\n</body>\n</html>\n```\n\n### Expected result\n\n```\ncontrol: <div></div>\ncandidate: <x-bio data-bio=\"<img src=x onerror=window.__candidateFired=true>\"></x-bio>\nafter connectedCallback: <x-bio data-bio=\"...\"><img src=\"x\" onerror=\"window.__candidateFired=true\"></x-bio>\ncontrol fired: false\ncandidate fired: true\n```\n\nThis is output of HTML PoC.\n\n<img width=\"1917\" height=\"961\" alt=\"poc\" src=\"https://github.com/user-attachments/assets/80e22989-5779-42f8-8ffb-106e9a4c2b10\" />\n\n\n## Impact\n\nThis does not appear to affect DOMPurify’s default configuration as a direct sanitizer bypass.\n\nThe impact is limited to applications that:\n\n- enable `CUSTOM_ELEMENT_HANDLING`,\n- rely on `afterSanitizeElements` as a security policy layer,\n- expect that hook to apply uniformly to all surviving elements,\n- and have allowed custom elements that later re-inject preserved attribute values into `innerHTML` or another HTML sink.\n\nIn that situation, the behavior can become a second-order XSS gadget because a security-relevant attribute is removed from normal elements but remains on allowed custom elements.\n\nPossible fixes or mitigations might include\n\n- ensuring that allowed custom elements also consistently pass through `afterSanitizeElements`\n- documenting clearly that elements preserved via `CUSTOM_ELEMENT_HANDLING` may not participate in the same post-element hook flow as normal allowlisted elements.",
5529+
"identifiers": {
5530+
"githubID": "GHSA-c2j3-45gr-mqc4"
5531+
},
5532+
"severity": "low",
5533+
"cwe": [
5534+
"CWE-184",
5535+
"CWE-693",
5536+
"CWE-79"
5537+
],
5538+
"info": [
5539+
"https://github.com/cure53/DOMPurify/security/advisories/GHSA-c2j3-45gr-mqc4",
5540+
"https://github.com/cure53/DOMPurify/pull/1537",
5541+
"https://github.com/cure53/DOMPurify/commit/a9ca1e537422319a557a9a2aa61f003b23b4a197",
5542+
"https://github.com/cure53/DOMPurify/releases/tag/3.4.12"
5543+
]
55195544
}
55205545
],
55215546
"extractors": {

‎repository/jsrepository-v2.json‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5880,6 +5880,26 @@
58805880
"info": [
58815881
"https://github.com/cure53/DOMPurify/security/advisories/GHSA-cmwh-pvxp-8882"
58825882
]
5883+
},
5884+
{
5885+
"atOrAbove": "0",
5886+
"below": "3.4.12",
5887+
"severity": "low",
5888+
"cwe": [
5889+
"CWE-184",
5890+
"CWE-693",
5891+
"CWE-79"
5892+
],
5893+
"identifiers": {
5894+
"summary": "DOMPurify: `CUSTOM_ELEMENT_HANDLING` bypasses `afterSanitizeElements` for allowed custom elements.",
5895+
"githubID": "GHSA-c2j3-45gr-mqc4"
5896+
},
5897+
"info": [
5898+
"https://github.com/cure53/DOMPurify/security/advisories/GHSA-c2j3-45gr-mqc4",
5899+
"https://github.com/cure53/DOMPurify/pull/1537",
5900+
"https://github.com/cure53/DOMPurify/commit/a9ca1e537422319a557a9a2aa61f003b23b4a197",
5901+
"https://github.com/cure53/DOMPurify/releases/tag/3.4.12"
5902+
]
58835903
}
58845904
],
58855905
"extractors": {

‎repository/jsrepository-v3.json‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5977,6 +5977,26 @@
59775977
"info": [
59785978
"https://github.com/cure53/DOMPurify/security/advisories/GHSA-cmwh-pvxp-8882"
59795979
]
5980+
},
5981+
{
5982+
"atOrAbove": "0",
5983+
"below": "3.4.12",
5984+
"severity": "low",
5985+
"cwe": [
5986+
"CWE-184",
5987+
"CWE-693",
5988+
"CWE-79"
5989+
],
5990+
"identifiers": {
5991+
"summary": "DOMPurify: `CUSTOM_ELEMENT_HANDLING` bypasses `afterSanitizeElements` for allowed custom elements.",
5992+
"githubID": "GHSA-c2j3-45gr-mqc4"
5993+
},
5994+
"info": [
5995+
"https://github.com/cure53/DOMPurify/security/advisories/GHSA-c2j3-45gr-mqc4",
5996+
"https://github.com/cure53/DOMPurify/pull/1537",
5997+
"https://github.com/cure53/DOMPurify/commit/a9ca1e537422319a557a9a2aa61f003b23b4a197",
5998+
"https://github.com/cure53/DOMPurify/releases/tag/3.4.12"
5999+
]
59806000
}
59816001
],
59826002
"extractors": {

‎repository/jsrepository-v4.json‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5976,6 +5976,26 @@
59765976
"info": [
59775977
"https://github.com/cure53/DOMPurify/security/advisories/GHSA-cmwh-pvxp-8882"
59785978
]
5979+
},
5980+
{
5981+
"atOrAbove": "0",
5982+
"below": "3.4.12",
5983+
"severity": "low",
5984+
"cwe": [
5985+
"CWE-184",
5986+
"CWE-693",
5987+
"CWE-79"
5988+
],
5989+
"identifiers": {
5990+
"summary": "DOMPurify: `CUSTOM_ELEMENT_HANDLING` bypasses `afterSanitizeElements` for allowed custom elements.",
5991+
"githubID": "GHSA-c2j3-45gr-mqc4"
5992+
},
5993+
"info": [
5994+
"https://github.com/cure53/DOMPurify/security/advisories/GHSA-c2j3-45gr-mqc4",
5995+
"https://github.com/cure53/DOMPurify/pull/1537",
5996+
"https://github.com/cure53/DOMPurify/commit/a9ca1e537422319a557a9a2aa61f003b23b4a197",
5997+
"https://github.com/cure53/DOMPurify/releases/tag/3.4.12"
5998+
]
59795999
}
59806000
],
59816001
"extractors": {

‎repository/jsrepository-v5-combined.json‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5983,6 +5983,26 @@
59835983
"info": [
59845984
"https://github.com/cure53/DOMPurify/security/advisories/GHSA-cmwh-pvxp-8882"
59855985
]
5986+
},
5987+
{
5988+
"atOrAbove": "0",
5989+
"below": "3.4.12",
5990+
"severity": "low",
5991+
"cwe": [
5992+
"CWE-184",
5993+
"CWE-693",
5994+
"CWE-79"
5995+
],
5996+
"identifiers": {
5997+
"summary": "DOMPurify: `CUSTOM_ELEMENT_HANDLING` bypasses `afterSanitizeElements` for allowed custom elements.",
5998+
"githubID": "GHSA-c2j3-45gr-mqc4"
5999+
},
6000+
"info": [
6001+
"https://github.com/cure53/DOMPurify/security/advisories/GHSA-c2j3-45gr-mqc4",
6002+
"https://github.com/cure53/DOMPurify/pull/1537",
6003+
"https://github.com/cure53/DOMPurify/commit/a9ca1e537422319a557a9a2aa61f003b23b4a197",
6004+
"https://github.com/cure53/DOMPurify/releases/tag/3.4.12"
6005+
]
59866006
}
59876007
],
59886008
"extractors": {

‎repository/jsrepository-v5.json‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5982,6 +5982,26 @@
59825982
"info": [
59835983
"https://github.com/cure53/DOMPurify/security/advisories/GHSA-cmwh-pvxp-8882"
59845984
]
5985+
},
5986+
{
5987+
"atOrAbove": "0",
5988+
"below": "3.4.12",
5989+
"severity": "low",
5990+
"cwe": [
5991+
"CWE-184",
5992+
"CWE-693",
5993+
"CWE-79"
5994+
],
5995+
"identifiers": {
5996+
"summary": "DOMPurify: `CUSTOM_ELEMENT_HANDLING` bypasses `afterSanitizeElements` for allowed custom elements.",
5997+
"githubID": "GHSA-c2j3-45gr-mqc4"
5998+
},
5999+
"info": [
6000+
"https://github.com/cure53/DOMPurify/security/advisories/GHSA-c2j3-45gr-mqc4",
6001+
"https://github.com/cure53/DOMPurify/pull/1537",
6002+
"https://github.com/cure53/DOMPurify/commit/a9ca1e537422319a557a9a2aa61f003b23b4a197",
6003+
"https://github.com/cure53/DOMPurify/releases/tag/3.4.12"
6004+
]
59856005
}
59866006
],
59876007
"extractors": {

0 commit comments

Comments
 (0)