From c3296fc7a3b42f86938924a11e7cdb254d8d919b Mon Sep 17 00:00:00 2001 From: Ady Date: Mon, 7 Sep 2026 22:25:57 +0200 Subject: [PATCH] fix: align fallback checks with serialized tag names --- lib/NodeUtils.js | 18 ++++++++++---- test/xss.js | 64 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/lib/NodeUtils.js b/lib/NodeUtils.js index 908adeb..405e2fc 100644 --- a/lib/NodeUtils.js +++ b/lib/NodeUtils.js @@ -145,14 +145,22 @@ function attrname(a) { return a.name; } +function serializedTagName(node) { + var ns = node.namespaceURI; + return (ns === NAMESPACE.HTML || ns === NAMESPACE.SVG || ns === NAMESPACE.MATHML) + ? node.localName + : node.tagName; +} + function fallbackRawContentTags(node) { const tags = []; while (node) { if (node.nodeType === 1 /*ELEMENT_NODE*/) { - if (node.namespaceURI === NAMESPACE.HTML && - (hasRawContentFallback[node.tagName] || - hasEscapableRawContent[node.tagName])) { - tags.push(node.localName); + const tagname = serializedTagName(node); + if (tagname && + (hasRawContentFallback[tagname.toUpperCase()] || + hasEscapableRawContent[tagname.toUpperCase()])) { + tags.push(tagname); } node = node.parentNode; } else if (node.nodeType === 11 /*DOCUMENT_FRAGMENT_NODE*/ && node._host) { @@ -295,7 +303,7 @@ function serializeOne(kid, parent) { case 1: //ELEMENT_NODE var ns = kid.namespaceURI; var html = ns === NAMESPACE.HTML; - var tagname = (html || ns === NAMESPACE.SVG || ns === NAMESPACE.MATHML) ? kid.localName : kid.tagName; + var tagname = serializedTagName(kid); s += '<' + tagname; diff --git a/test/xss.js b/test/xss.js index 58ce90e..0c2d882 100644 --- a/test/xss.js +++ b/test/xss.js @@ -1001,6 +1001,70 @@ exports.fallbackRawTextProcessingInstructionEscapesAncestorClosingTag = async fu } }; +exports.fallbackRawTextProcessingInstructionUsesSerializedAncestorName = async function () { + const cases = [ + { + label: 'SVG noembed', + namespace: 'http://www.w3.org/2000/svg', + qualifiedName: 'noembed', + serializedName: 'noembed', + }, + { + label: 'MathML noembed', + namespace: 'http://www.w3.org/1998/Math/MathML', + qualifiedName: 'noembed', + serializedName: 'noembed', + }, + { + label: 'SVG iframe', + namespace: 'http://www.w3.org/2000/svg', + qualifiedName: 'iframe', + serializedName: 'iframe', + }, + { + label: 'qualified HTML iframe', + namespace: 'http://www.w3.org/1999/xhtml', + qualifiedName: 'x:iframe', + serializedName: 'iframe', + }, + ]; + + for (const testCase of cases) { + const document = domino.createDocument(''); + const fallbackEl = document.createElementNS( + testCase.namespace, + testCase.qualifiedName, + ); + + fallbackEl.appendChild( + document.createProcessingInstruction( + 'x', + ``, + `${testCase.label}: matching fallback closing tag was not escaped: ${serialized}`, + ); + + const alerted = await alertFired(serialized); + alerted.should.equal( + false, + `alert fired for PI under ${testCase.label}: ${serialized}`, + ); + } +}; + exports.commentNodeEscapesAbruptClosingComment = async function () { // A comment content that starts with `>` or `->` is closed by the parser // right away (the "abrupt-closing-of-empty-comment" parse error), so the rest