diff --git a/src/ngSanitize/sanitize.js b/src/ngSanitize/sanitize.js index eae696396d08..6c4402433557 100644 --- a/src/ngSanitize/sanitize.js +++ b/src/ngSanitize/sanitize.js @@ -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 = ''; + if (!inertBodyElement.querySelector('svg')) { + return getInertBodyElement_XHR; + } else { + // Check for the Firefox bug - which prevents the inner img JS from being sanitized + inertBodyElement.innerHTML = '

'; + 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 `` do not get hoisted to the `` tag. + html = '' + 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 `` do not get hoisted to the `` tag. + html = '' + 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; diff --git a/test/ngSanitize/sanitizeSpec.js b/test/ngSanitize/sanitizeSpec.js index 0e3b1f6a0627..70682c23ed4d 100644 --- a/test/ngSanitize/sanitizeSpec.js +++ b/test/ngSanitize/sanitizeSpec.js @@ -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(''); + 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('

'); + expect(doc).toEqual('

'); + })); describe('SVG support', function() { @@ -273,7 +287,6 @@ describe('HTML', function() { $sanitizeProvider.enableSvg(true); })); - it('should accept SVG tags', function() { expectHTML('') .toBeOneOf('',