Skip to content
This repository has been archived by the owner on Aug 1, 2024. It is now read-only.

Commit

Permalink
Use createElement on inert document to prevent unexpected subresour…
Browse files Browse the repository at this point in the history
…ce loads.

RELNOTES: n/a

PiperOrigin-RevId: 575162808
Change-Id: I555c1d86b3ef7c8fa297931bbb057ad44eba5f99
  • Loading branch information
Closure Team authored and copybara-github committed Oct 20, 2023
1 parent 11b2f3c commit da6ff88
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 86 deletions.
12 changes: 8 additions & 4 deletions closure/goog/html/sanitizer/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,11 @@ closure_js_library(
lenient = True,
deps = [
":csspropertysanitizer",
":inertdocument",
":noclobber",
"//closure/goog/array",
"//closure/goog/dom",
"//closure/goog/dom:safe",
"//closure/goog/dom:tagname",
"//closure/goog/html:cssspecificity",
"//closure/goog/html:safestyle",
"//closure/goog/html:safestylesheet",
Expand Down Expand Up @@ -75,8 +75,6 @@ closure_js_library(
":tagwhitelist",
"//closure/goog/array",
"//closure/goog/asserts",
"//closure/goog/dom",
"//closure/goog/dom:tagname",
"//closure/goog/functions",
"//closure/goog/html:safehtml",
"//closure/goog/html:safestyle",
Expand All @@ -100,17 +98,23 @@ closure_js_library(
],
)

closure_js_library(
name = "inertdocument",
srcs = ["inertdocument.js"],
lenient = True,
)

closure_js_library(
name = "safedomtreeprocessor",
srcs = ["safedomtreeprocessor.js"],
lenient = True,
deps = [
":elementweakmap",
":inertdocument",
":noclobber",
"//closure/goog/dom",
"//closure/goog/dom:nodetype",
"//closure/goog/dom:safe",
"//closure/goog/dom:tagname",
"//closure/goog/html:uncheckedconversions",
"//closure/goog/log",
"//closure/goog/string:const",
Expand Down
31 changes: 4 additions & 27 deletions closure/goog/html/sanitizer/csssanitizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ goog.provide('goog.html.sanitizer.CssSanitizer');

goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.dom.safe');
goog.require('goog.html.CssSpecificity');
goog.require('goog.html.SafeStyle');
goog.require('goog.html.SafeStyleSheet');
goog.require('goog.html.SafeUrl');
goog.require('goog.html.sanitizer.CssPropertySanitizer');
goog.require('goog.html.sanitizer.inertDocument');
goog.require('goog.html.sanitizer.noclobber');
goog.require('goog.html.uncheckedconversions');
goog.require('goog.object');
Expand Down Expand Up @@ -279,9 +279,9 @@ goog.html.sanitizer.CssSanitizer.sanitizeInlineStyleString = function(
return goog.html.SafeStyle.EMPTY;
}

var div = goog.html.sanitizer.CssSanitizer
.createInertDocument_()
.createElement('DIV');
const div =
goog.html.sanitizer.inertDocument.createInertDocument().createElement(
'DIV');
div.style.cssText = cssText;
return goog.html.sanitizer.CssSanitizer.sanitizeInlineStyle(
div.style, opt_uriRewriter);
Expand Down Expand Up @@ -392,29 +392,6 @@ goog.html.sanitizer.CssSanitizer.mergeStyleDeclarations_ = function(
});
};


/**
* Creates an DOM Document object that will not execute scripts or make
* network requests while parsing HTML.
* @return {!Document}
* @private
*/
goog.html.sanitizer.CssSanitizer.createInertDocument_ = function() {
'use strict';
// Documents created using window.document.implementation.createHTMLDocument()
// use the same custom component registry as their parent document. This means
// that parsing arbitrary HTML can result in calls to user-defined JavaScript.
// This is worked around by creating a template element and its content's
// document. See https://github.com/cure53/DOMPurify/issues/47.
var doc = document;
if (typeof HTMLTemplateElement === 'function') {
doc =
goog.dom.createElement(goog.dom.TagName.TEMPLATE).content.ownerDocument;
}
return doc.implementation.createHTMLDocument('');
};


/**
* Provides a cross-browser way to get a CSS property names.
* @param {!CSSStyleDeclaration} cssStyle A CSS style object.
Expand Down
36 changes: 0 additions & 36 deletions closure/goog/html/sanitizer/csssanitizer_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -400,42 +400,6 @@ testSuite({
assertContains('url("http://foo.com/a")', output);
},

/** @suppress {accessControls} */
testInertDocument() {
if (!document.implementation.createHTMLDocument) {
return; // skip test
}

/**
* @suppress {strictMissingProperties} suppression added to enable type
* checking
*/
window.xssFiredInertDocument = false;
const doc = CssSanitizer.createInertDocument_();
try {
doc.write('<script> window.xssFiredInertDocument = true; </script>');
} catch (e) {
// ignore
}
assertFalse(window.xssFiredInertDocument);
},

/** @suppress {accessControls} */
testInertCustomElements() {
if (typeof HTMLTemplateElement != 'function' || !document.registerElement) {
return; // skip test
}

const inertDoc = CssSanitizer.createInertDocument_();
const xFooConstructor = document.registerElement('x-foo');
const xFooElem =
document.implementation.createHTMLDocument('').createElement('x-foo');
assertTrue(xFooElem instanceof xFooConstructor); // sanity check

const inertXFooElem = inertDoc.createElement('x-foo');
assertFalse(inertXFooElem instanceof xFooConstructor);
},

testSanitizeStyleSheetString_basic() {
let input = '';
assertSanitizedCssEquals(input, input);
Expand Down
6 changes: 2 additions & 4 deletions closure/goog/html/sanitizer/htmlsanitizer.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ goog.provide('goog.html.sanitizer.HtmlSanitizerUrlPolicy');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.functions');
goog.require('goog.html.SafeHtml');
goog.require('goog.html.SafeStyle');
Expand Down Expand Up @@ -1243,12 +1241,12 @@ goog.html.sanitizer.HtmlSanitizer.prototype.createElementWithoutAttributes =
}
if (this.tagWhitelist_[dirtyName]) {
// If it's whitelisted, keep as is.
return document.createElement(dirtyName);
return this.inertDocument_.createElement(dirtyName);
}
// If it's neither blacklisted nor whitelisted, replace with span. If the
// relevant builder option is enabled, the tag will bear the original tag
// name in a data attribute.
const spanElement = goog.dom.createElement(goog.dom.TagName.SPAN);
const spanElement = this.inertDocument_.createElement('span');
if (this.shouldAddOriginalTagNames_) {
goog.html.sanitizer.noclobber.setElementAttribute(
spanElement, goog.html.sanitizer.HTML_SANITIZER_SANITIZED_ATTR_NAME_,
Expand Down
21 changes: 21 additions & 0 deletions closure/goog/html/sanitizer/htmlsanitizer_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -1592,4 +1592,25 @@ testSuite({
new Builder().allowStyleTag().withStyleContainer().build());
},

async testNotLoadSubresources() {
// Sanitization by itself should not load subresources.
const html = `<img src=ftp://not-load-subresources />`;

const sanitizer = new Builder()
.withCustomNetworkRequestUrlPolicy(SafeUrl.sanitize)
.build();

sanitizer.sanitize(html);

// Give the subresource a little time to load.
await new Promise(resolve => {
setTimeout(resolve, 200);
});

// Make sure there was no attempt to load the subresource.
const entry = performance.getEntries().find(
entry => entry.name.startsWith('ftp://not-load-subresources'));
assertUndefined(entry);
}

});
25 changes: 25 additions & 0 deletions closure/goog/html/sanitizer/inertdocument.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* @license
* Copyright The Closure Library Authors.
* SPDX-License-Identifier: Apache-2.0
*/

/**
* @fileoverview Exports a method to create an inert document, which will not
* execute JS or make network requests while parsing HTML.
*/

goog.module('goog.html.sanitizer.inertDocument');
goog.module.declareLegacyNamespace();

/**
* Creates an DOM Document object that will not execute scripts or make
* network requests while parsing HTML.
* @return {!Document}
*/
function createInertDocument() {
'use strict';
return document.implementation.createHTMLDocument('');
}

exports = {createInertDocument};
32 changes: 32 additions & 0 deletions closure/goog/html/sanitizer/inertdocument_test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* @license
* Copyright The Closure Library Authors.
* SPDX-License-Identifier: Apache-2.0
*/

/** @fileoverview testcases for createInertDocument. */

goog.module('goog.html.inertDocumentTests');
goog.setTestOnly();

const testSuite = goog.require('goog.testing.testSuite');
const {createInertDocument} = goog.require('goog.html.sanitizer.inertDocument');

testSuite({
testInertDocument() {
if (!document.implementation.createHTMLDocument) {
return; // skip test
}

/**
* @suppress {strictMissingProperties} suppression added to enable type
* checking
*/
window.xssFiredInertDocument = false;
const doc = createInertDocument();
const script = doc.createElement('script');
script.text = 'window.xssFiredInertDocument = true';
doc.body.appendChild(script);
assertFalse(window.xssFiredInertDocument);
},
});
34 changes: 19 additions & 15 deletions closure/goog/html/sanitizer/safedomtreeprocessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,17 @@
goog.module('goog.html.sanitizer.SafeDomTreeProcessor');
goog.module.declareLegacyNamespace();

var Const = goog.require('goog.string.Const');
var ElementWeakMap = goog.require('goog.html.sanitizer.ElementWeakMap');
var Logger = goog.require('goog.log.Logger');
var NodeType = goog.require('goog.dom.NodeType');
var TagName = goog.require('goog.dom.TagName');
var googDom = goog.require('goog.dom');
var googLog = goog.require('goog.log');
var noclobber = goog.require('goog.html.sanitizer.noclobber');
var safe = goog.require('goog.dom.safe');
var uncheckedconversions = goog.require('goog.html.uncheckedconversions');
var userAgent = goog.require('goog.userAgent');
const Const = goog.require('goog.string.Const');
const ElementWeakMap = goog.require('goog.html.sanitizer.ElementWeakMap');
const Logger = goog.require('goog.log.Logger');
const NodeType = goog.require('goog.dom.NodeType');
const googDom = goog.require('goog.dom');
const googLog = goog.require('goog.log');
const noclobber = goog.require('goog.html.sanitizer.noclobber');
const safe = goog.require('goog.dom.safe');
const uncheckedconversions = goog.require('goog.html.uncheckedconversions');
const userAgent = goog.require('goog.userAgent');
const {createInertDocument} = goog.require('goog.html.sanitizer.inertDocument');

/** @const {?Logger} */
var logger = googLog.getLogger('goog.html.sanitizer.SafeDomTreeProcessor');
Expand Down Expand Up @@ -88,7 +88,10 @@ function getDomTreeWalker(html) {
* attributes, etc.
* @constructor @struct @abstract
*/
var SafeDomTreeProcessor = function() {};
const SafeDomTreeProcessor = function() {
/** @protected @const {!Document} */
this.inertDocument_ = createInertDocument();
};

/**
* Parses an HTML string and walks the resulting DOM forest to apply the
Expand All @@ -109,7 +112,7 @@ SafeDomTreeProcessor.prototype.processToString = function(html) {
// attached attributes to it. To do so, we make a new SPAN tag the parent of
// the existing root span tag, so that the rest of the function will remove
// that one instead.
var newRoot = googDom.createElement(TagName.SPAN);
const newRoot = this.inertDocument_.createElement('span');
newRoot.appendChild(newTree);
newTree = newRoot;
}
Expand All @@ -130,10 +133,11 @@ SafeDomTreeProcessor.prototype.processToString = function(html) {
* @protected @final
*/
SafeDomTreeProcessor.prototype.processToTree = function(html) {
const newRoot = /** @type {!HTMLSpanElement} */ (
this.inertDocument_.createElement('span'));
if (!SAFE_PARSING_SUPPORTED) {
return googDom.createElement(TagName.SPAN);
return newRoot;
}
var newRoot = googDom.createElement(TagName.SPAN);
// Allow subclasses to attach properties to the root.
this.processRoot(newRoot);

Expand Down

0 comments on commit da6ff88

Please sign in to comment.