diff --git a/lib/NodeUtils.js b/lib/NodeUtils.js index a0a4962..325311e 100644 --- a/lib/NodeUtils.js +++ b/lib/NodeUtils.js @@ -135,11 +135,17 @@ function attrname(a) { function fallbackRawContentTags(node) { const tags = []; - while (node?.nodeType === 1 /*ELEMENT_NODE*/) { - if (node.namespaceURI === NAMESPACE.HTML && hasRawContentFallback[node.tagName]) { - tags.push(node.localName); + while (node) { + if (node.nodeType === 1 /*ELEMENT_NODE*/) { + if (node.namespaceURI === NAMESPACE.HTML && hasRawContentFallback[node.tagName]) { + tags.push(node.localName); + } + node = node.parentNode; + } else if (node.nodeType === 11 /*DOCUMENT_FRAGMENT_NODE*/ && node._host) { + node = node._host; + } else { + node = node.parentNode; } - node = node.parentNode; } return tags; } @@ -309,7 +315,13 @@ function serializeOne(kid, parent) { s += ''; break; case 7: //PROCESSING_INSTRUCTION_NODE - const content = escapeProcessingInstructionContent(kid.data); + let content = escapeProcessingInstructionContent(kid.data); + if (content.includes(''; break; case 10: //DOCUMENT_TYPE_NODE diff --git a/lib/htmlelts.js b/lib/htmlelts.js index 923f7f9..c2a941d 100644 --- a/lib/htmlelts.js +++ b/lib/htmlelts.js @@ -1141,10 +1141,21 @@ define({ ctor: function HTMLTemplateElement(doc, localName, prefix) { HTMLElement.call(this, doc, localName, prefix); this._contentFragment = doc._templateDoc.createDocumentFragment(); + this._contentFragment._host = this; }, props: { content: { get: function() { return this._contentFragment; } }, - serialize: { value: function() { return this.content.serialize(); } } + serialize: { value: function() { return this.content.serialize(); } }, + cloneNode: { + value: function(deep) { + var clone = HTMLElement.prototype.cloneNode.call(this, deep); + if (deep) { + clone._contentFragment = this._contentFragment.cloneNode(true); + clone._contentFragment._host = clone; + } + return clone; + } + } } }); diff --git a/test/domino.js b/test/domino.js index 916e09d..bf0b33b 100644 --- a/test/domino.js +++ b/test/domino.js @@ -1556,3 +1556,72 @@ exports.worksWithBase64DataImages = function () { 'iVBORw0KGgoAAAANSUhEUgAAAAgAAAAIAQMAAAD+wSzIAAAABlBMVEX///+/v7+jQ3Y5AAAADklEQVQI12P4AIX8EAgALgAD/aNpbtEAAAAASUVORK5CYII);">' ); }; + +exports.processingInstructionClosingTagEscapedInNoscript = function () { + const document = domino.createDocument(''); + const noscript = document.createElement('noscript'); + noscript.appendChild(document.createProcessingInstruction('x', '' + ); + html.should.not.containEql(''); + html.should.not.containEql(''; + template.content.appendChild(xmp); + fallback.appendChild(template); + document.body.appendChild(fallback); + + const html = document.body.serialize(); + html.should.equal( + '<' + tagName + '>' + ); + document.body.removeChild(fallback); + } +}; + +exports.clonedTemplateContentMaintainsHostReference = function () { + const document = domino.createDocument(''); + const fallback = document.createElement('noscript'); + const template = document.createElement('template'); + const xmp = document.createElement('xmp'); + xmp.textContent = ''; + template.content.appendChild(xmp); + + const clonedTemplate = template.cloneNode(true); + fallback.appendChild(clonedTemplate); + document.body.appendChild(fallback); + + const html = document.body.serialize(); + html.should.equal( + '' + ); +}; + diff --git a/test/xss.js b/test/xss.js index 3988b28..aa71a01 100644 --- a/test/xss.js +++ b/test/xss.js @@ -869,3 +869,121 @@ exports.fallbackRawTextForeignContentEscapesAncestorClosingTag = async function } } }; + +exports.fallbackRawTextTemplateContentEscapesAncestorClosingTag = async function () { + const fallbackTags = ['noscript', 'iframe', 'noembed', 'noframes']; + + for (const tag of fallbackTags) { + // noscript/iframe/noembed/noframes > template > xmp + { + const document = domino.createDocument(''); + const fallbackEl = document.createElement(tag); + const template = document.createElement('template'); + const xmp = document.createElement('xmp'); + xmp.textContent = ``; + template.content.appendChild(xmp); + fallbackEl.appendChild(template); + document.body.appendChild(fallbackEl); + + const serialized = document.body.serialize(); + serialized.should.equal( + `<${tag}>`, + ); + + const reparsedDoc = domino.createDocument('' + serialized + ''); + reparsedDoc.getElementsByTagName('img').length.should.equal(0); + + const reparsed = reparsedDoc.body.innerHTML; + reparsed.should.not.containEql(`: ` + reparsed, + ); + } + + // noscript/iframe/noembed/noframes > template > #comment + { + const document = domino.createDocument(''); + const fallbackEl = document.createElement(tag); + const template = document.createElement('template'); + const comment = document.createComment(``); + template.content.appendChild(comment); + fallbackEl.appendChild(template); + document.body.appendChild(fallbackEl); + + const serialized = document.body.serialize(); + serialized.should.equal( + `<${tag}>`, + ); + + const reparsedDoc = domino.createDocument('' + serialized + ''); + reparsedDoc.getElementsByTagName('img').length.should.equal(0); + + const reparsed = reparsedDoc.body.innerHTML; + reparsed.should.not.containEql(`: ` + reparsed, + ); + } + } +}; + +exports.fallbackRawTextProcessingInstructionEscapesAncestorClosingTag = async function () { + const fallbackTags = ['noscript', 'iframe', 'noembed', 'noframes']; + + for (const tag of fallbackTags) { + // noscript/iframe/noembed/noframes > ProcessingInstruction + { + const document = domino.createDocument(''); + const fallbackEl = document.createElement(tag); + fallbackEl.appendChild(document.createProcessingInstruction('x', ``)); + document.body.appendChild(fallbackEl); + + const serialized = document.body.serialize(); + serialized.should.equal( + `<${tag}>`, + ); + + const reparsedDoc = domino.createDocument('' + serialized + ''); + reparsedDoc.getElementsByTagName('img').length.should.equal(0); + + const reparsed = reparsedDoc.body.innerHTML; + reparsed.should.not.containEql(`: ` + reparsed, + ); + } + + // noscript/iframe/noembed/noframes > template > ProcessingInstruction + { + const document = domino.createDocument(''); + const fallbackEl = document.createElement(tag); + const template = document.createElement('template'); + template.content.appendChild(document.createProcessingInstruction('x', ``)); + fallbackEl.appendChild(template); + document.body.appendChild(fallbackEl); + + const serialized = document.body.serialize(); + serialized.should.equal( + `<${tag}>`, + ); + + const reparsedDoc = domino.createDocument('' + serialized + ''); + reparsedDoc.getElementsByTagName('img').length.should.equal(0); + + const reparsed = reparsedDoc.body.innerHTML; + reparsed.should.not.containEql(`: ` + reparsed, + ); + } + } +}; +