Initial commit
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+361
@@ -0,0 +1,361 @@
|
||||
'use strict';
|
||||
var domino = require('../lib');
|
||||
var puppeteer = require("puppeteer");
|
||||
var NodeUtils = require('../lib/NodeUtils');
|
||||
|
||||
exports = exports.xss = {};
|
||||
|
||||
// Tests for HTML serialization concentrating on possible "Mutation based
|
||||
// XSS vectors"; see https://cure53.de/fp170.pdf
|
||||
|
||||
// If we change HTML serialization such that any of these tests fail, please
|
||||
// review the change very carefully for potential XSS vectors!
|
||||
|
||||
async function alertFired(html) {
|
||||
let alerted = false;
|
||||
const page = await incognito.newPage();
|
||||
page.on("dialog", async dialog => {
|
||||
alerted = true;
|
||||
await dialog.accept();
|
||||
});
|
||||
await page.goto("data:text/html," + html, {waitUntil: 'load'});
|
||||
return alerted;
|
||||
}
|
||||
|
||||
/** @type {puppeteer.Browser} */
|
||||
let browser;
|
||||
/** @type {puppeteer.BrowserContext} */
|
||||
let incognito;
|
||||
|
||||
exports.before = async function() {
|
||||
browser = await puppeteer.launch({headless:"new"});
|
||||
incognito = await browser.createIncognitoBrowserContext();
|
||||
}
|
||||
|
||||
exports.after = async function() {
|
||||
await incognito.close();
|
||||
await browser.close();
|
||||
}
|
||||
|
||||
exports.fp170_31 = function() {
|
||||
var document = domino.createDocument(
|
||||
'<img src="test.jpg" alt="``onload=xss()" />'
|
||||
);
|
||||
// In particular, ensure alt attribute is quoted, not: ...alt=``onload=xss()
|
||||
document.body.innerHTML.should.equal(
|
||||
'<img src="test.jpg" alt="``onload=xss()">'
|
||||
);
|
||||
};
|
||||
|
||||
exports.fp170_32 = function() {
|
||||
var document = domino.createDocument(
|
||||
'<article xmlns="urn:img src=x onerror=xss()//">123'
|
||||
);
|
||||
// XXX check XML serialization as well, once that's implemented
|
||||
// In particular, ensure that the xmlns string isn't used as an XML prefix
|
||||
// when serializing (and, of course, that attribute value is quoted)
|
||||
document.body.innerHTML.should.equal(
|
||||
'<article xmlns="urn:img src=x onerror=xss()//">123</article>'
|
||||
);
|
||||
};
|
||||
|
||||
exports.fp170_33 = function() {
|
||||
var document = domino.createDocument(
|
||||
'<p style="font -family:\'ar\\27\\3bx\\3aexpression\\28xss\\28\\29\\29\\3bial\'"></p>'
|
||||
);
|
||||
// Be sure domino doesn't decode the backslash escapes
|
||||
// (especially in the future if we parse the CSS values more fully)
|
||||
document.body.innerHTML.should.equal(
|
||||
'<p style="font -family:\'ar\\27\\3bx\\3aexpression\\28xss\\28\\29\\29\\3bial\'"></p>'
|
||||
);
|
||||
};
|
||||
|
||||
exports.fp170_34 = function() {
|
||||
var document = domino.createDocument(
|
||||
'<p style="font -family:\'ar";x=expression(xss())/*ial\'"></p>'
|
||||
);
|
||||
// Be sure domino re-encodes the entities correctly
|
||||
// (especially in the future if we parse the CSS values more fully)
|
||||
document.body.innerHTML.should.equal(
|
||||
'<p style="font -family:\'ar";x=expression(xss())/*ial\'"></p>'
|
||||
);
|
||||
};
|
||||
|
||||
exports.fp170_35 = function() {
|
||||
var document = domino.createDocument(
|
||||
'<img style="font-fa\\22onload\\3dxss\\28\\29\\20mily:\'arial\'" src="test.jpg" />'
|
||||
);
|
||||
// Again, ensure domino doesn't decode the backslash escapes
|
||||
// (especially in the future if we parse the CSS values more fully)
|
||||
document.body.innerHTML.should.equal(
|
||||
'<img style="font-fa\\22onload\\3dxss\\28\\29\\20mily:\'arial\'" src="test.jpg">'
|
||||
);
|
||||
};
|
||||
|
||||
exports.fp170_36 = function() {
|
||||
var document = domino.createDocument(
|
||||
'<style>*{font-family:\'ar<img src="test.jpg" onload="xss()"/>ial\'}</style>'
|
||||
);
|
||||
// Ensure that HTML entities are properly encoded inside <style>
|
||||
document.head.innerHTML.should.equal(
|
||||
'<style>*{font-family:\'ar<img src="test.jpg" onload="xss()"/>ial\'}</style>'
|
||||
);
|
||||
};
|
||||
|
||||
exports.fp170_37 = function() {
|
||||
var document = domino.createDocument(
|
||||
'<p><svg><style>*{font-family:\'</style><img/src=x	onerror=xss()//\'}</style></svg></p>'
|
||||
);
|
||||
// Ensure that HTML entities are properly encoded inside <style>
|
||||
document.body.innerHTML.should.equal(
|
||||
'<p><svg><style>*{font-family:\'</style><img/src=x\tonerror=xss()//\'}</style></svg></p>'
|
||||
);
|
||||
};
|
||||
|
||||
exports.escapeAngleBracketsInDivAttr = function() {
|
||||
var document = domino.createDocument(
|
||||
`<div>You don't have JS! Click<a href="#" title="Search for </div><script>alert(1)</script> without JS">here</a> to go to the no-js website.</div>`
|
||||
);
|
||||
document.body.innerHTML.should.equal(
|
||||
`<div>You don't have JS! Click<a href="#" title="Search for </div><script>alert(1)</script> without JS">here</a> to go to the no-js website.</div>`
|
||||
);
|
||||
};
|
||||
|
||||
exports.escapeAngleBracketsInNoScriptAttr = function() {
|
||||
var document = domino.createDocument(
|
||||
`<div><noscript>You don't have JS! Click<a href="#" title="Search for </noscript><script>alert(1)</script> without JS">here</a> to go to the no-js website.</noscript></div>`
|
||||
);
|
||||
document.body.innerHTML.should.equal(
|
||||
`<div><noscript>You don't have JS! Click<a href="#" title="Search for </noscript><script>alert(1)</script> without JS">here</a> to go to the no-js website.</noscript></div>`
|
||||
);
|
||||
};
|
||||
|
||||
exports.styleMatchingClosingTagInRawText = function() {
|
||||
const document = domino.createDocument('');
|
||||
const style = document.createElement("style");
|
||||
style.textContent = "abc</style><script>alert(1)</script>";
|
||||
document.body.appendChild(style);
|
||||
|
||||
// Ensure that HTML entities are properly encoded inside <style>
|
||||
document.body.serialize().should.equal(
|
||||
'<style>abc</style><script>alert(1)</script></style>'
|
||||
);
|
||||
|
||||
const html = document.serialize();
|
||||
return alertFired(html).should.eventually.be.false('alert fired for: ' + html);
|
||||
};
|
||||
|
||||
exports.styleMatchingClosingTagSkipsInsideCommentedContent = function() {
|
||||
const document = domino.createDocument('');
|
||||
const style = document.createElement("style");
|
||||
style.textContent = "abc<!--</style>--><script>alert(1)</script>";
|
||||
document.body.appendChild(style);
|
||||
|
||||
document.body.serialize().should.equal(
|
||||
'<style>abc<!--</style>--><script>alert(1)</script></style>'
|
||||
);
|
||||
|
||||
const html = document.serialize();
|
||||
return alertFired(html).should.eventually.be.false('alert fired for: ' + html);
|
||||
};
|
||||
|
||||
exports.styleMatchingClosingTagAfterClosingComment = function() {
|
||||
const document = domino.createDocument('');
|
||||
const style = document.createElement("style");
|
||||
style.textContent = "abc--></style><script>alert(1)</script>";
|
||||
document.body.appendChild(style);
|
||||
|
||||
// Ensure that HTML entities are properly encoded inside <style>
|
||||
document.body.serialize().should.equal(
|
||||
'<style>abc--></style><script>alert(1)</script></style>'
|
||||
);
|
||||
|
||||
const html = document.serialize();
|
||||
return alertFired(html).should.eventually.be.false('alert fired for: ' + html);
|
||||
};
|
||||
|
||||
exports.styleMatchingClosingTagSkipsUnclosedCommentedContent = function() {
|
||||
const document = domino.createDocument('');
|
||||
const style = document.createElement("style");
|
||||
style.textContent = "abc<!--</style><script>alert(1)</script>";
|
||||
document.body.appendChild(style);
|
||||
|
||||
document.body.serialize().should.equal(
|
||||
'<style>abc<!--</style><script>alert(1)</script></style>'
|
||||
);
|
||||
|
||||
const html = document.serialize();
|
||||
return alertFired(html).should.eventually.be.false('alert fired for: ' + html);
|
||||
};
|
||||
|
||||
exports.scriptMatchingClosingTagInRawText = function() {
|
||||
const document = domino.createDocument('');
|
||||
const script = document.createElement("script");
|
||||
script.textContent = "abc</script><script>alert(1)</script>";
|
||||
document.body.appendChild(script);
|
||||
|
||||
// Ensure that HTML entities are properly encoded inside <script>
|
||||
// Note: the `</script>` is encoded in both places.
|
||||
document.body.serialize().should.equal(
|
||||
'<script>abc</script><script>alert(1)</script></script>'
|
||||
);
|
||||
|
||||
const html = document.serialize();
|
||||
return alertFired(html).should.eventually.be.false('alert fired for: ' + html);
|
||||
};
|
||||
|
||||
exports.oneRawTextTagInsideAnotherOne = function() {
|
||||
const document = domino.createDocument('');
|
||||
const xmp = document.createElement("xmp");
|
||||
const style = document.createElement("style");
|
||||
xmp.textContent = "</style><script>alert(1)</script>";
|
||||
style.appendChild(xmp);
|
||||
document.body.appendChild(style);
|
||||
|
||||
document.body.serialize().should.equal(
|
||||
'<style><xmp></style><script>alert(1)</script></xmp></style>'
|
||||
);
|
||||
|
||||
const html = document.serialize();
|
||||
return alertFired(html).should.eventually.be.false('alert fired for: ' + html);
|
||||
}
|
||||
|
||||
exports.xssInAttributeInsideRawTextTag = function() {
|
||||
const document = domino.createDocument('');
|
||||
const xmp = document.createElement("xmp");
|
||||
const div = document.createElement("div");
|
||||
div.title = "</xmp><script>alert(1)</script>";
|
||||
xmp.appendChild(div);
|
||||
document.body.appendChild(xmp);
|
||||
|
||||
document.body.serialize().should.equal(
|
||||
'<xmp><div title="</xmp><script>alert(1)</script>"></div></xmp>'
|
||||
);
|
||||
|
||||
const html = document.serialize();
|
||||
return alertFired(html).should.eventually.be.false('alert fired for: ' + html);
|
||||
}
|
||||
|
||||
exports.commentNodeInsideRawTextTag = function() {
|
||||
const document = domino.createDocument('');
|
||||
const xmp = document.createElement("xmp");
|
||||
const comment = document.createComment('</xmp><script>alert(1)</script>');
|
||||
xmp.appendChild(comment);
|
||||
document.body.appendChild(xmp);
|
||||
|
||||
document.body.serialize().should.equal(
|
||||
'<xmp><!--</xmp><script>alert(1)</script>--></xmp>'
|
||||
);
|
||||
|
||||
const html = document.serialize();
|
||||
return alertFired(html).should.eventually.be.false('alert fired for: ' + html);
|
||||
}
|
||||
|
||||
exports.alternativeEndTagForRawTextTag = function() {
|
||||
const document = domino.createDocument('');
|
||||
const style = document.createElement("style");
|
||||
style.textContent = "</style /foobar><script>alert(1)</script>";
|
||||
document.body.appendChild(style);
|
||||
|
||||
document.body.serialize().should.equal(
|
||||
'<style></style /foobar><script>alert(1)</script></style>'
|
||||
);
|
||||
|
||||
const html = document.serialize();
|
||||
return alertFired(html).should.eventually.be.false('alert fired for: ' + html);
|
||||
}
|
||||
|
||||
exports.badCommentNode = function() {
|
||||
const document = domino.createDocument('');
|
||||
const comment = document.createComment('--><script>alert(1)</script>');
|
||||
document.body.appendChild(comment);
|
||||
|
||||
document.body.serialize().should.equal(
|
||||
'<!----><script>alert(1)</script>-->'
|
||||
);
|
||||
|
||||
const html = document.serialize();
|
||||
return alertFired(html).should.eventually.be.false('alert fired for: ' + html);
|
||||
}
|
||||
|
||||
exports.anotherBadCommentNode = function() {
|
||||
const document = domino.createDocument('');
|
||||
const comment = document.createComment('--!><script>alert(1)</script>');
|
||||
document.body.appendChild(comment);
|
||||
|
||||
document.body.serialize().should.equal(
|
||||
'<!----!><script>alert(1)</script>-->'
|
||||
);
|
||||
|
||||
const html = document.serialize();
|
||||
return alertFired(html).should.eventually.be.false('alert fired for: ' + html);
|
||||
}
|
||||
|
||||
exports.badProcessingInstruction = function() {
|
||||
const document = domino.createDocument('');
|
||||
const pi = document.createProcessingInstruction("bad", "><script>alert(1)</script>");
|
||||
document.body.appendChild(pi);
|
||||
|
||||
document.body.serialize().should.equal(
|
||||
'<?bad ><script>alert(1)</script>?>'
|
||||
);
|
||||
|
||||
const html = document.serialize();
|
||||
return alertFired(html).should.eventually.be.false('alert fired for: ' + html);
|
||||
}
|
||||
|
||||
exports.verifyEscapeMatchingClosingTag = function() {
|
||||
const cases = [
|
||||
['', 'style', ''], // no artifacts while processing an empty string
|
||||
['abc', 'script', 'abc'], // no artifacts while processing a string without closing tags
|
||||
['</style /foobar>abc', 'style', '</style /foobar>abc'],
|
||||
['</xmp><script>alert(1)</script>', 'xmp', '</xmp><script>alert(1)</script>'],
|
||||
['"</xmp>"', 'xmp', '"</xmp>"'],
|
||||
|
||||
// Raw content element inside another raw content element.
|
||||
['<xmp></style><script>alert(1)</script></xmp>', 'style',
|
||||
'<xmp></style><script>alert(1)</script></xmp>'],
|
||||
|
||||
['abc</script><script>alert(1)</script>', 'script',
|
||||
'abc</script><script>alert(1)</script>'],
|
||||
|
||||
// No changes to the content in case there are no matching closing tags.
|
||||
['<xmp></style><script>alert(1)</script></xmp>', 'iframe',
|
||||
'<xmp></style><script>alert(1)</script></xmp>'],
|
||||
];
|
||||
for (const [rawContent, parentTag, expected] of cases) {
|
||||
NodeUtils.ɵescapeMatchingClosingTag(rawContent, parentTag).should.equal(expected);
|
||||
}
|
||||
}
|
||||
|
||||
exports.verifyEscapeClosingCommentTag = function() {
|
||||
const cases = [
|
||||
['', ''], // no artifacts while processing an empty string
|
||||
['abc', 'abc'], // no artifacts while processing a string without closing tags
|
||||
['a-->bc-->', 'a-->bc-->'],
|
||||
['a--!>bc--!>', 'a--!>bc--!>'],
|
||||
['a- -> b c - ->', 'a- -> b c - ->'],
|
||||
['a- -!> b c - -!>', 'a- -!> b c - -!>'],
|
||||
['<!--a--!> <!--b--!>', '<!--a--!> <!--b--!>'],
|
||||
['<!--a--> <!--b-->', '<!--a--> <!--b-->'],
|
||||
['<!--a--< <!--b--<', '<!--a--< <!--b--<'],
|
||||
];
|
||||
for (const [rawContent, expected] of cases) {
|
||||
NodeUtils.ɵescapeClosingCommentTag(rawContent).should.equal(expected);
|
||||
}
|
||||
}
|
||||
|
||||
exports.verifyEscapeProcessingInstructionContent = function() {
|
||||
const cases = [
|
||||
['', ''], // no artifacts while processing an empty string
|
||||
['abc', 'abc'], // no artifacts while processing a string without `>` chars
|
||||
['>>>', '>>>'],
|
||||
['<<<', '<<<'],
|
||||
['><script>alert(1)</script>', '><script>alert(1)</script>'],
|
||||
['<!--a-->', '<!--a-->'],
|
||||
['">"', '">"'],
|
||||
];
|
||||
for (const [rawContent, expected] of cases) {
|
||||
NodeUtils.ɵescapeProcessingInstructionContent(rawContent).should.equal(expected);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user