dompurify
Version:
DOMPurify is a DOM-only, super-fast, uber-tolerant XSS sanitizer for HTML, MathML and SVG. It's written in JavaScript and works in all modern browsers (Safari, Opera (15+), Internet Explorer (10+), Firefox and Chrome - as well as almost anything else usin
263 lines (261 loc) • 20 kB
JavaScript
module.exports = function(DOMPurify, tests, xssTests) {
QUnit
.cases(tests)
.test( 'Sanitization test', function(params, assert) {
assert.contains( DOMPurify.sanitize( params.payload ), params.expected, 'Payload: ' + params.payload);
});
// Config-Flag Tests
QUnit.test( 'Config-Flag tests: KEEP_CONTENT + ALLOWED_TAGS / ALLOWED_ATTR', function(assert) {
// KEEP_CONTENT + ALLOWED_TAGS / ALLOWED_ATTR
assert.equal( DOMPurify.sanitize( '<iframe>Hello</iframe>', {KEEP_CONTENT: false} ), "");
assert.contains( DOMPurify.sanitize( '<a href="#">abc<b style="color:red">123</b><q class="cite">123</b></a>', {ALLOWED_TAGS: ['b', 'q'], ALLOWED_ATTR: ['style'], KEEP_CONTENT: true}),
["abc<b style=\"color:red\">123</b><q>123</q>", "abc<b style=\"color: red;\">123</b><q>123</q>"]
);
assert.equal( DOMPurify.sanitize( '<a href="#">abc<b style="color:red">123</b><q class="cite">123</b></a>', {ALLOWED_TAGS: ['b', 'q'], ALLOWED_ATTR: ['style'], KEEP_CONTENT: false}), "");
assert.equal( DOMPurify.sanitize( '<a href="#">abc</a>', {ALLOWED_TAGS: ['b', 'q'], KEEP_CONTENT: false}), "");
assert.equal( DOMPurify.sanitize( '<form><input name="parentNode"></form>', {ALLOWED_TAGS: ['input'], KEEP_CONTENT: true}), "<input>" );
});
QUnit.test( 'Config-Flag tests: ALLOW_DATA_ATTR', function(assert) {
// ALLOW_DATA_ATTR
assert.equal( DOMPurify.sanitize( '<a href="#" data-abc\"="foo">abc</a>', {ALLOW_DATA_ATTR: true}), "<a href=\"#\">abc</a>" );
assert.equal( DOMPurify.sanitize( '<a href="#" data-abc="foo">abc</a>', {ALLOW_DATA_ATTR: false}), "<a href=\"#\">abc</a>" );
assert.contains( DOMPurify.sanitize( '<a href="#" data-abc="foo">abc</a>', {ALLOW_DATA_ATTR: true}),
["<a data-abc=\"foo\" href=\"#\">abc</a>", "<a href=\"#\" data-abc=\"foo\">abc</a>"]
);
assert.contains( DOMPurify.sanitize( '<a href="#" data-abc-1-2-3="foo">abc</a>', {ALLOW_DATA_ATTR: true}),
["<a data-abc-1-2-3=\"foo\" href=\"#\">abc</a>", "<a href=\"#\" data-abc-1-2-3=\"foo\">abc</a>"]
);
assert.equal( DOMPurify.sanitize( '<a href="#" data-""="foo">abc</a>', {ALLOW_DATA_ATTR: true}), "<a href=\"#\">abc</a>" );
assert.contains( DOMPurify.sanitize( '<a href="#" data-äöü="foo">abc</a>', {ALLOW_DATA_ATTR: true}),
["<a href=\"#\" data-äöü=\"foo\">abc</a>", "<a data-äöü=\"foo\" href=\"#\">abc</a>"]
);
assert.contains( DOMPurify.sanitize( '<a href="#" data-\u00B7._="foo">abc</a>', {ALLOW_DATA_ATTR: true}),
["<a data-\u00B7._=\"foo\" href=\"#\">abc</a>", "<a href=\"#\">abc</a>"] // IE11 and Edge throw an InvalidCharacterError
);
assert.equal( DOMPurify.sanitize( '<a href="#" data-\u00B5="foo">abc</a>', {ALLOW_DATA_ATTR: true}), "<a href=\"#\">abc</a>" );
});
QUnit.test( 'Config-Flag tests: ADD_TAGS', function(assert) {
// ADD_TAGS
assert.equal( DOMPurify.sanitize( '<my-component>abc</my-component>', {ADD_TAGS: ['my-component']}), "<my-component>abc</my-component>" );
assert.equal( DOMPurify.sanitize( '<my-ĸompønent>abc</my-ĸompønent>', {ADD_TAGS: ['my-ĸompønent']}), "<my-ĸompønent>abc</my-ĸompønent>" );
});
QUnit.test( 'Config-Flag tests: ADD_TAGS + ADD_ATTR', function(assert) {
// ADD_TAGS + ADD_ATTR
assert.equal( DOMPurify.sanitize( '<my-component my-attr="foo">abc</my-component>', {ADD_TAGS: ['my-component']}), "<my-component>abc</my-component>" );
assert.equal( DOMPurify.sanitize( '<my-component my-attr="foo">abc</my-component>', {ADD_TAGS: ['my-component'], ADD_ATTR: ['my-attr']}), "<my-component my-attr=\"foo\">abc</my-component>" );
assert.equal( DOMPurify.sanitize( '<my-ĸompønent my-æŧŧr="foo">abc</my-ĸompønent>', {ADD_TAGS: ['my-ĸompønent']}), "<my-ĸompønent>abc</my-ĸompønent>" );
assert.equal( DOMPurify.sanitize( '<my-ĸompønent my-æŧŧr="foo">abc</my-ĸompønent>', {ADD_TAGS: ['my-ĸompønent'], ADD_ATTR: ['my-æŧŧr']}), "<my-ĸompønent my-æŧŧr=\"foo\">abc</my-ĸompønent>" );
});
QUnit.test( 'Config-Flag tests: SAFE_FOR_JQUERY', function(assert) {
//SAFE_FOR_JQUERY
assert.equal( DOMPurify.sanitize( '<a>123</a><option><style><img src=x onerror=alert(1)>', {SAFE_FOR_JQUERY: false}), "<a>123</a><option><style><img src=x onerror=alert(1)></style></option>" );
assert.equal( DOMPurify.sanitize( '<a>123</a><option><style><img src=x onerror=alert(1)>', {SAFE_FOR_JQUERY: true}), "<a>123</a><option><style><img src=x onerror=alert(1)></style></option>" );
assert.equal( DOMPurify.sanitize( '<option><style></option></select><b><img src=xx: onerror=alert(1)></style></option>', {SAFE_FOR_JQUERY: false}), "<option><style></option></select><b><img src=xx: onerror=alert(1)></style></option>" );
assert.equal( DOMPurify.sanitize( '<option><style></option></select><b><img src=xx: onerror=alert(1)></style></option>', {SAFE_FOR_JQUERY: true}), "<option><style></option></select><b><img src=xx: onerror=alert(1)></style></option>" );
assert.equal( DOMPurify.sanitize( '<option><iframe></select><b><script>alert(1)<\/script>', {SAFE_FOR_JQUERY: false, KEEP_CONTENT: false}), "<option></option>" );
assert.equal( DOMPurify.sanitize( '<option><iframe></select><b><script>alert(1)<\/script>', {SAFE_FOR_JQUERY: true, KEEP_CONTENT: false}), "<option></option>" );
assert.equal( DOMPurify.sanitize( '<b><style><style/><img src=xx: onerror=alert(1)>', {SAFE_FOR_JQUERY: false}), "<b><style><style/><img src=xx: onerror=alert(1)></style></b>" );
assert.equal( DOMPurify.sanitize( '<b><style><style/><img src=xx: onerror=alert(1)>', {SAFE_FOR_JQUERY: true}), "<b><style><style/><img src=xx: onerror=alert(1)></style></b>" );
assert.contains( DOMPurify.sanitize( '1<template><s>000</s></template>2', {SAFE_FOR_JQUERY: true}), ["1<template><s>000</s></template>2", "1<template></template>2"] );
assert.contains( DOMPurify.sanitize( '<template><s>000</s></template>', {SAFE_FOR_JQUERY: true}), ["", "<template><s>000</s></template>"]);
});
QUnit.test( 'Config-Flag tests: SAFE_FOR_TEMPLATES', function(assert) {
//SAFE_FOR_TEMPLATES
assert.equal( DOMPurify.sanitize( '<a>123{{456}}<b><style><% alert(1) %></style>456</b></a>', {SAFE_FOR_TEMPLATES: true}), "<a> <b><style> </style>456</b></a>" );
assert.equal( DOMPurify.sanitize( '<a data-bind="style: alert(1)"></a>', {SAFE_FOR_TEMPLATES: true}), "<a></a>" );
assert.equal( DOMPurify.sanitize( '<a data-harmless=""></a>', {SAFE_FOR_TEMPLATES: true, ALLOW_DATA_ATTR: true}), "<a></a>" );
assert.equal( DOMPurify.sanitize( '<a data-harmless=""></a>', {SAFE_FOR_TEMPLATES: false, ALLOW_DATA_ATTR: false}), "<a></a>" );
assert.equal( DOMPurify.sanitize( '<a>{{123}}{{456}}<b><style><% alert(1) %><% 123 %></style>456</b></a>', {SAFE_FOR_TEMPLATES: true}), "<a> <b><style> </style>456</b></a>" );
assert.equal( DOMPurify.sanitize( '<a>{{123}}abc{{456}}<b><style><% alert(1) %>def<% 123 %></style>456</b></a>', {SAFE_FOR_TEMPLATES: true}), "<a> <b><style> </style>456</b></a>" );
assert.equal( DOMPurify.sanitize( '<a>123{{45{{6}}<b><style><% alert(1)%> %></style>456</b></a>', {SAFE_FOR_TEMPLATES: true}), "<a> <b><style> </style>456</b></a>" );
assert.equal( DOMPurify.sanitize( '<a>123{{45}}6}}<b><style><% <%alert(1) %></style>456</b></a>', {SAFE_FOR_TEMPLATES: true}), "<a> <b><style> </style>456</b></a>" );
assert.equal( DOMPurify.sanitize( '<a>123{{<b>456}}</b><style><% alert(1) %></style>456</a>', {SAFE_FOR_TEMPLATES: true}), "<a>123 <b> </b><style> </style>456</a>" );
assert.contains( DOMPurify.sanitize( '<b>{{evil<script>alert(1)</script><form><img src=x name=textContent></form>}}</b>', {SAFE_FOR_TEMPLATES: true}),
["<b> </b>", "<b> </b>", "<b> <form><img src=\"x\"></form> </b>"]
);
assert.contains( DOMPurify.sanitize( '<b>he{{evil<script>alert(1)</script><form><img src=x name=textContent></form>}}ya</b>', {SAFE_FOR_TEMPLATES: true}),
["<b>he ya</b>", "<b>he </b>", "<b>he <form><img src=\"x\"></form> ya</b>"] // Investigate on Safari 8!
);
assert.equal( DOMPurify.sanitize( '<a>123<% <b>456}}</b><style>{{ alert(1) }}</style>456 %></a>', {SAFE_FOR_TEMPLATES: true}), "<a>123 <b> </b><style> </style> </a>" );
});
QUnit.test( 'Config-Flag tests: SANITIZE_DOM', function(assert) {
// SANITIZE_DOM
assert.equal( DOMPurify.sanitize( '<form name="window">', {SANITIZE_DOM: true}), "<form></form>" );
assert.equal( DOMPurify.sanitize( '<img src="x" name="implementation">', {SANITIZE_DOM: true}), '<img src="x">' );
assert.equal( DOMPurify.sanitize( '<img src="x" name="createNodeIterator">', {SANITIZE_DOM: true}), '<img src="x">' );
assert.equal( DOMPurify.sanitize( '<img src="x" name="getElementById">', {SANITIZE_DOM: false}), "<img name=\"getElementById\" src=\"x\">" );
assert.equal( DOMPurify.sanitize( '<img src="x" name="getElementById">', {SANITIZE_DOM: true}), "<img src=\"x\">" );
assert.equal( DOMPurify.sanitize( '<a href="x" id="location">click</a>', {SANITIZE_DOM: true}), "<a href=\"x\">click</a>" );
assert.contains( DOMPurify.sanitize( '<form><input name="attributes"></form>', {ADD_TAGS: ['form'], SANITIZE_DOM: false}),
["", "<form><input name=\"attributes\"></form>"]
);
assert.contains( DOMPurify.sanitize( '<form><input name="attributes"></form>', {ADD_TAGS: ['form'], SANITIZE_DOM: true}),
["", "<form><input name=\"attributes\"></form>", "<form><input></form>"]
);
});
QUnit.test( 'Config-Flag tests: WHOLE_DOCUMENT', function(assert) {
//WHOLE_DOCUMENT
assert.equal( DOMPurify.sanitize( '123', {WHOLE_DOCUMENT: false}), "123" );
assert.equal( DOMPurify.sanitize( '123', {WHOLE_DOCUMENT: true}), "<html><head></head><body>123</body></html>" );
assert.equal( DOMPurify.sanitize( '<style>*{color:red}</style>', {WHOLE_DOCUMENT: false}), "" );
assert.equal( DOMPurify.sanitize( '<style>*{color:red}</style>', {WHOLE_DOCUMENT: true}), "<html><head><style>*{color:red}</style></head><body></body></html>" );
assert.equal( DOMPurify.sanitize( '123<style>*{color:red}</style>', {WHOLE_DOCUMENT: false}), "123<style>*{color:red}</style>" );
assert.equal( DOMPurify.sanitize( '123<style>*{color:red}</style>', {WHOLE_DOCUMENT: true}), "<html><head></head><body>123<style>*{color:red}</style></body></html>" );
});
QUnit.test( 'Config-Flag tests: RETURN_DOM', function(assert) {
//RETURN_DOM
assert.equal( DOMPurify.sanitize( '<a>123<b>456</b></a>', {RETURN_DOM: true}).outerHTML, "<body><a>123<b>456</b></a></body>" );
assert.equal( DOMPurify.sanitize( '<a>123<b>456<script>alert(1)<\/script></b></a>', {RETURN_DOM: true}).outerHTML, "<body><a>123<b>456</b></a></body>" );
assert.equal( DOMPurify.sanitize( '<a>123<b>456</b></a>', {RETURN_DOM: true, WHOLE_DOCUMENT: true}).outerHTML, "<html><head></head><body><a>123<b>456</b></a></body></html>" );
assert.equal( DOMPurify.sanitize( '<a>123<b>456<script>alert(1)<\/script></b></a>', {RETURN_DOM: true, WHOLE_DOCUMENT: true}).outerHTML, "<html><head></head><body><a>123<b>456</b></a></body></html>" );
assert.equal( DOMPurify.sanitize( '123', {RETURN_DOM: true}).outerHTML, "<body>123</body>" );
});
QUnit.test( 'Config-Flag tests: RETURN_DOM_IMPORT', function(assert) {
//RETURN_DOM_IMPORT
assert.notEqual( DOMPurify.sanitize( '123', {RETURN_DOM : true }).ownerDocument, document );
assert.notEqual( DOMPurify.sanitize( '123', {RETURN_DOM : true, RETURN_DOM_IMPORT: false}).ownerDocument, document );
assert.equal ( DOMPurify.sanitize( '123', {RETURN_DOM : true, RETURN_DOM_IMPORT: true }).ownerDocument, document );
assert.notEqual( DOMPurify.sanitize( '123', {RETURN_DOM_FRAGMENT: true }).ownerDocument, document );
assert.notEqual( DOMPurify.sanitize( '123', {RETURN_DOM_FRAGMENT: true, RETURN_DOM_IMPORT: false}).ownerDocument, document );
assert.equal ( DOMPurify.sanitize( '123', {RETURN_DOM_FRAGMENT: true, RETURN_DOM_IMPORT: true }).ownerDocument, document );
});
QUnit.test( 'Config-Flag tests: RETURN_DOM_FRAGMENT', function(assert) {
//RETURN_DOM_FRAGMENT
// attempt clobbering
var fragment = DOMPurify.sanitize( 'foo<img id="createDocumentFragment">', {RETURN_DOM_FRAGMENT: true});
assert.equal(fragment.nodeType, 11);
assert.notEqual(fragment.ownerDocument, document);
assert.equal(fragment.firstChild && fragment.firstChild.nodeValue, 'foo');
// again, but without SANITIZE_DOM
fragment = DOMPurify.sanitize( 'foo<img id="createDocumentFragment">', {RETURN_DOM_FRAGMENT: true, SANITIZE_DOM: false});
assert.equal(fragment.nodeType, 11);
assert.notEqual(fragment.ownerDocument, document);
assert.equal(fragment.firstChild && fragment.firstChild.nodeValue, 'foo');
});
QUnit.test( 'Config-Flag tests: FORBID_TAGS', function(assert) {
//FORBID_TAGS
assert.equal( DOMPurify.sanitize( '<a>123<b>456</b></a>', {FORBID_TAGS: ['b']}), "<a>123456</a>" );
assert.equal( DOMPurify.sanitize( '<a>123<b>456<script>alert(1)<\/script></b></a>789', {FORBID_TAGS: ['a', 'b']}), "123456789" );
assert.equal( DOMPurify.sanitize( '<a>123<b>456</b></a>', {FORBID_TAGS: ['c']}), "<a>123<b>456</b></a>" );
assert.equal( DOMPurify.sanitize( '<a>123<b>456<script>alert(1)<\/script></b></a>789', {FORBID_TAGS: ['script', 'b']}), "<a>123456</a>789" );
assert.equal( DOMPurify.sanitize( '<a>123<b>456</b></a>', {ADD_TAGS: ['b'], FORBID_TAGS: ['b']}), "<a>123456</a>" );
});
QUnit.test( 'Config-Flag tests: FORBID_ATTR', function(assert) {
//FORBID_ATTR
assert.equal( DOMPurify.sanitize( '<a x="1">123<b>456</b></a>', {FORBID_ATTR: ['x']}), "<a>123<b>456</b></a>" );
assert.equal( DOMPurify.sanitize( '<a class="0" x="1">123<b y="1">456<script>alert(1)<\/script></b></a>789', {FORBID_ATTR: ['x', 'y']}), "<a class=\"0\">123<b>456</b></a>789" );
assert.equal( DOMPurify.sanitize( '<a y="1">123<b y="1" y="2">456</b></a>', {FORBID_ATTR: ['y']}), "<a>123<b>456</b></a>" );
assert.equal( DOMPurify.sanitize( '<a>123<b x="1">456<script y="1">alert(1)<\/script></b></a>789', {FORBID_ATTR: ['x', 'y']}), "<a>123<b>456</b></a>789" );
});
QUnit.test( 'Test dirty being an array', function(assert) {
assert.equal( DOMPurify.sanitize( ['<a>123<b>456</b></a>']), "<a>123<b>456</b></a>" );
assert.equal( DOMPurify.sanitize( ['<img src=', 'x onerror=alert(1)>']), "<img src=\",x\">" );
});
// XSS tests: Native DOM methods (alert() should not be called)
QUnit
.cases(xssTests)
.asyncTest('XSS test: native', function(params, assert) {
document.getElementById( 'qunit-fixture' ).innerHTML = DOMPurify.sanitize( params.payload );
setTimeout(function() {
QUnit.start();
assert.notEqual( window.xssed, true, 'alert() was called' );
// Teardown
document.getElementById( 'qunit-fixture' ).innerHTML = '';
window.xssed = false;
}, 100);
});
// XSS tests: jQuery (alert() should not be called)
QUnit
.cases(xssTests)
.asyncTest('XSS test: jQuery', function(params, assert) {
jQuery( '#qunit-fixture' ).html( DOMPurify.sanitize( params.payload, {SAFE_FOR_JQUERY: true} ) );
setTimeout(function() {
QUnit.start();
assert.notEqual( window.xssed, true, 'alert() was called' );
// Teardown
jQuery( '#qunit-fixture' ).empty();
window.xssed = false;
}, 100);
});
// document.write tests to handle FF's strange behavior
QUnit
.cases(xssTests)
.asyncTest('XSS test: document.write() into iframe', function(params, assert) {
var iframe = document.createElement('iframe');
iframe.src='about:blank';
iframe.onload=function(){
QUnit.start();
iframe.contentDocument.write('<script>window.alert=function(){top.xssed=true;}</script>' + DOMPurify.sanitize( params.payload ));
assert.notEqual( window.xssed, true, 'alert() was called from document.write()' );
window.xssed = false;
iframe.parentNode.removeChild(iframe);
}
document.body.appendChild(iframe);
});
// cross-check that document.write into iframe works properly
QUnit
.asyncTest('XSS test: document.write() into iframe', function(assert) {
window.xssed = false;
var iframe = document.createElement('iframe');
iframe.src='about:blank';
iframe.onload=function(){
QUnit.start();
iframe.contentDocument.write('<script>window.alert=function(){parent.xssed=true;}</script><script>alert(1);</script>' );
assert.equal( window.xssed, true, 'alert() was called but not detected' );
window.xssed = false;
iframe.parentNode.removeChild(iframe);
}
document.body.appendChild(iframe);
});
// Check for isSupported property
QUnit.test( 'DOMPurify property tests', function(assert) {
assert.equal( typeof DOMPurify.isSupported, 'boolean' );
});
// Test with a custom window object
QUnit.test( 'DOMPurify custom window tests', function(assert) {
assert.strictEqual(typeof DOMPurify(null).version, 'string');
assert.strictEqual(DOMPurify(null).isSupported, false);
assert.strictEqual(DOMPurify(null).sanitize, undefined);
assert.strictEqual(typeof DOMPurify({}).version, 'string');
assert.strictEqual(DOMPurify({}).isSupported, false);
assert.strictEqual(DOMPurify({}).sanitize, undefined);
assert.strictEqual(typeof DOMPurify({document: 'not really a document'}).version, 'string');
assert.strictEqual(DOMPurify({document: 'not really a document'}).isSupported, false);
assert.strictEqual(DOMPurify({document: 'not really a document'}).sanitize, undefined);
assert.strictEqual(typeof DOMPurify(window).version, 'string');
assert.strictEqual(typeof DOMPurify(window).sanitize, 'function');
});
// Test to prevent security issues with pre-clobbered DOM
QUnit.test( 'sanitize() should not throw if the original document is clobbered _after_ DOMPurify has been instantiated', function(assert) {
var evilNode = document.createElement('div');
evilNode.innerHTML = '<img id="implementation"><img id="createNodeIterator"><img id="importNode"><img id="createElement">';
document.body.appendChild(evilNode);
try {
// tests implementation and createNodeIterator
var resultPlain = DOMPurify.sanitize('123');
// tests importNode
var resultImport = DOMPurify.sanitize( '123', {RETURN_DOM : true, RETURN_DOM_IMPORT: true });
// tests createElement
var resultBody = DOMPurify.sanitize( '123<img id="body">');
} finally {
// clean up before doing the actual assertions, otherwise qunit/jquery/etc might blow up
document.body.removeChild(evilNode);
}
assert.equal( resultPlain, '123' );
assert.equal( resultImport.ownerDocument, document );
assert.equal( resultBody, '123<img>' );
} );
// Test to check against a hang in MSIE (#89)
QUnit.test( 'sanitize() should not hang on MSIE when hook changes textContent', function(assert) {
DOMPurify.addHook('afterSanitizeElements', function(node) {
if (node.nodeType && node.nodeType === document.TEXT_NODE) {
node.textContent = 'foo';
}
return node;
});
var dirty = '<div><p>This is a beatufiul text</p><p>This is too</p></div>';
var modified = '<div><p>foo</p><p>foo</p></div>';
assert.equal(modified, DOMPurify.sanitize(dirty));
DOMPurify.removeHooks('afterSanitizeElements')
} );
}