fix($sanitize): use appropriate inert document strategy for Firefox and Safari
Both Firefox and Safari are vulnerable to XSS if we use an inert document created via `document.implementation.createHTMLDocument()`. Now we check for those vulnerabilities and then use a DOMParser or XHR strategy if needed. Thanks to @cure53 for the heads up on this issue.
This commit is contained in:
committed by
Pete Bacon Darwin
parent
e65928eecb
commit
7673ca7d15
+75
-15
@@ -313,16 +313,78 @@ function $SanitizeProvider() {
|
||||
return obj;
|
||||
}
|
||||
|
||||
var inertBodyElement = (function(window) {
|
||||
var doc;
|
||||
if (window.document && window.document.implementation) {
|
||||
doc = window.document.implementation.createHTMLDocument('inert');
|
||||
/**
|
||||
* Create an inert document that contains the dirty HTML that needs sanitizing
|
||||
* Depending upon browser support we use one of three strategies for doing this.
|
||||
* Support: Safari 10.x -> XHR strategy
|
||||
* Support: Firefox -> DomParser strategy
|
||||
*/
|
||||
var getInertBodyElement /* function(html: string): HTMLBodyElement */ = (function(window, document) {
|
||||
var inertDocument;
|
||||
if (document && document.implementation) {
|
||||
inertDocument = document.implementation.createHTMLDocument('inert');
|
||||
} else {
|
||||
throw $sanitizeMinErr('noinert', 'Can\'t create an inert html document');
|
||||
}
|
||||
var docElement = doc.documentElement || doc.getDocumentElement();
|
||||
return docElement.getElementsByTagName('body')[0];
|
||||
})(window);
|
||||
var inertBodyElement = (inertDocument.documentElement || inertDocument.getDocumentElement()).querySelector('body');
|
||||
|
||||
// Check for the Safari 10.1 bug - which allows JS to run inside the SVG G element
|
||||
inertBodyElement.innerHTML = '<svg><g onload="this.parentNode.remove()"></g></svg>';
|
||||
if (!inertBodyElement.querySelector('svg')) {
|
||||
return getInertBodyElement_XHR;
|
||||
} else {
|
||||
// Check for the Firefox bug - which prevents the inner img JS from being sanitized
|
||||
inertBodyElement.innerHTML = '<svg><p><style><img src="</style><img src=x onerror=alert(1)//">';
|
||||
if (inertBodyElement.querySelector('svg img')) {
|
||||
return getInertBodyElement_DOMParser;
|
||||
} else {
|
||||
return getInertBodyElement_InertDocument;
|
||||
}
|
||||
}
|
||||
|
||||
function getInertBodyElement_XHR(html) {
|
||||
// We add this dummy element to ensure that the rest of the content is parsed as expected
|
||||
// e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the `<head>` tag.
|
||||
html = '<remove></remove>' + html;
|
||||
try {
|
||||
html = encodeURI(html);
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
var xhr = new window.XMLHttpRequest();
|
||||
xhr.responseType = 'document';
|
||||
xhr.open('GET', 'data:text/html;charset=utf-8,' + html, false);
|
||||
xhr.send(null);
|
||||
var body = xhr.response.body;
|
||||
body.firstChild.remove();
|
||||
return body;
|
||||
}
|
||||
|
||||
function getInertBodyElement_DOMParser(html) {
|
||||
// We add this dummy element to ensure that the rest of the content is parsed as expected
|
||||
// e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the `<head>` tag.
|
||||
html = '<remove></remove>' + html;
|
||||
try {
|
||||
var body = new window.DOMParser().parseFromString(html, 'text/html').body;
|
||||
body.firstChild.remove();
|
||||
return body;
|
||||
} catch (e) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function getInertBodyElement_InertDocument(html) {
|
||||
inertBodyElement.innerHTML = html;
|
||||
|
||||
// Support: IE 9-11 only
|
||||
// strip custom-namespaced attributes on IE<=11
|
||||
if (document.documentMode) {
|
||||
stripCustomNsAttrs(inertBodyElement);
|
||||
}
|
||||
|
||||
return inertBodyElement;
|
||||
}
|
||||
})(window, window.document);
|
||||
|
||||
/**
|
||||
* @example
|
||||
@@ -342,7 +404,9 @@ function $SanitizeProvider() {
|
||||
} else if (typeof html !== 'string') {
|
||||
html = '' + html;
|
||||
}
|
||||
inertBodyElement.innerHTML = html;
|
||||
|
||||
var inertBodyElement = getInertBodyElement(html);
|
||||
if (!inertBodyElement) return '';
|
||||
|
||||
//mXSS protection
|
||||
var mXSSAttempts = 5;
|
||||
@@ -352,13 +416,9 @@ function $SanitizeProvider() {
|
||||
}
|
||||
mXSSAttempts--;
|
||||
|
||||
// Support: IE 9-11 only
|
||||
// strip custom-namespaced attributes on IE<=11
|
||||
if (window.document.documentMode) {
|
||||
stripCustomNsAttrs(inertBodyElement);
|
||||
}
|
||||
html = inertBodyElement.innerHTML; //trigger mXSS
|
||||
inertBodyElement.innerHTML = html;
|
||||
// trigger mXSS if it is going to happen by reading and writing the innerHTML
|
||||
html = inertBodyElement.innerHTML;
|
||||
inertBodyElement = getInertBodyElement(html);
|
||||
} while (html !== inertBodyElement.innerHTML);
|
||||
|
||||
var node = inertBodyElement.firstChild;
|
||||
|
||||
@@ -49,6 +49,8 @@ describe('HTML', function() {
|
||||
comment = comment_;
|
||||
}
|
||||
};
|
||||
// Trigger the $sanitizer provider to execute, which initializes the `htmlParser` function.
|
||||
inject(function($sanitize) {});
|
||||
});
|
||||
|
||||
it('should not parse comments', function() {
|
||||
@@ -266,6 +268,18 @@ describe('HTML', function() {
|
||||
});
|
||||
});
|
||||
|
||||
// See https://github.com/cure53/DOMPurify/blob/a992d3a75031cb8bb032e5ea8399ba972bdf9a65/src/purify.js#L439-L449
|
||||
it('should not allow JavaScript execution when creating inert document', inject(function($sanitize) {
|
||||
var doc = $sanitize('<svg><g onload="window.xxx = 100"></g></svg>');
|
||||
expect(window.xxx).toBe(undefined);
|
||||
delete window.xxx;
|
||||
}));
|
||||
|
||||
// See https://github.com/cure53/DOMPurify/releases/tag/0.6.7
|
||||
it('should not allow JavaScript hidden in badly formed HTML to get through sanitization (Firefox bug)', inject(function($sanitize) {
|
||||
var doc = $sanitize('<svg><p><style><img src="</style><img src=x onerror=alert(1)//">');
|
||||
expect(doc).toEqual('<p><img src="x"></p>');
|
||||
}));
|
||||
|
||||
describe('SVG support', function() {
|
||||
|
||||
@@ -273,7 +287,6 @@ describe('HTML', function() {
|
||||
$sanitizeProvider.enableSvg(true);
|
||||
}));
|
||||
|
||||
|
||||
it('should accept SVG tags', function() {
|
||||
expectHTML('<svg width="400px" height="150px" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red"></svg>')
|
||||
.toBeOneOf('<svg width="400px" height="150px" xmlns="http://www.w3.org/2000/svg"><circle cx="50" cy="50" r="40" stroke="black" stroke-width="3" fill="red"></circle></svg>',
|
||||
|
||||
Reference in New Issue
Block a user