Skip to content

Commit

Permalink
Add support for TrustedTypes (#5692)
Browse files Browse the repository at this point in the history
* Add support for TrustedTypes to html-tag.js

This avoids setting innerHTML to a string.

* Comment on why .slice() the  XMLSerializer output

* Handle data binding Trusted Types into attributes

Fixes #5648

* Lint clean
  • Loading branch information
rictic authored Aug 26, 2021
1 parent 1c0153d commit 10220c9
Show file tree
Hide file tree
Showing 3 changed files with 75 additions and 8 deletions.
3 changes: 2 additions & 1 deletion .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"Polymer": true,
"ShadyDOM": true,
"ShadyCSS": true,
"JSCompiler_renameProperty": true
"JSCompiler_renameProperty": true,
"trustedTypes": true
}
}
16 changes: 16 additions & 0 deletions lib/mixins/property-accessors.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ while (proto) {
proto = Object.getPrototypeOf(proto);
}

const isTrustedType = (() => {
if (!window.trustedTypes) {
return () => false;
}
return (val) => trustedTypes.isHTML(val) ||
trustedTypes.isScript(val) || trustedTypes.isScriptURL(val);
})();

/**
* Used to save the value of a property that will be overridden with
* an accessor. If the `model` is a prototype, the values will be saved
Expand Down Expand Up @@ -215,6 +223,14 @@ export const PropertyAccessors = dedupingMixin(superClass => {
if (value instanceof Date) {
return value.toString();
} else if (value) {
if (isTrustedType(value)) {
/**
* Here `value` isn't actually a string, but it should be
* passed into APIs that normally expect a string, like
* elem.setAttribute.
*/
return /** @type {?} */ (value);
}
try {
return JSON.stringify(value);
} catch(x) {
Expand Down
64 changes: 57 additions & 7 deletions lib/utils/html-tag.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,33 @@ subject to an additional IP rights grant found at http://polymer.github.io/PATEN
*/
import './boot.js';

/**
* Our TrustedTypePolicy for HTML which is declared using the Polymer html
* template tag function.
*
* That HTML is a developer-authored constant, and is parsed with innerHTML
* before any untrusted expressions have been mixed in. Therefor it is
* considered safe by construction.
*
* @type {!TrustedTypePolicy|undefined}
*/
const policy = window.trustedTypes &&
trustedTypes.createPolicy('polymer-html-literal', {createHTML: (s) => s});

/**
* Class representing a static string value which can be used to filter
* strings by asseting that they have been created via this class. The
* `value` property returns the string passed to the constructor.
*/
class LiteralString {
constructor(string) {
/**
* @param {!ITemplateArray} strings Constant parts of tagged template literal
* @param {!Array<*>} values Variable parts of tagged template literal
*/
constructor(strings, values) {
assertValidTemplateStringParameters(strings, values);
const string = values.reduce(
(acc, v, idx) => acc + literalValue(v) + strings[idx + 1], strings[0]);
/** @type {string} */
this.value = string.toString();
}
Expand Down Expand Up @@ -48,7 +68,15 @@ function literalValue(value) {
*/
function htmlValue(value) {
if (value instanceof HTMLTemplateElement) {
return /** @type {!HTMLTemplateElement } */(value).innerHTML;
// Use the XML serializer to avoid xMSS attacks from browsers' sometimes
// unexpected formatting / cleanup of innerHTML.
const serializedNewTree = new XMLSerializer().serializeToString(
/** @type {!HTMLTemplateElement } */ (value));
// The XMLSerializer is similar to .outerHTML, so slice off the leading
// and trailing parts of the <template> wrapper tag.
return serializedNewTree.slice(
serializedNewTree.indexOf('>') + 1,
serializedNewTree.lastIndexOf('</'));
} else if (value instanceof LiteralString) {
return literalValue(value);
} else {
Expand Down Expand Up @@ -92,12 +120,35 @@ function htmlValue(value) {
* @return {!HTMLTemplateElement} Constructed HTMLTemplateElement
*/
export const html = function html(strings, ...values) {
const template = /** @type {!HTMLTemplateElement} */(document.createElement('template'));
template.innerHTML = values.reduce((acc, v, idx) =>
acc + htmlValue(v) + strings[idx + 1], strings[0]);
assertValidTemplateStringParameters(strings, values);
const template =
/** @type {!HTMLTemplateElement} */ (document.createElement('template'));
let value = values.reduce(
(acc, v, idx) => acc + htmlValue(v) + strings[idx + 1], strings[0]);
if (policy) {
value = policy.createHTML(value);
}
template.innerHTML = value;
return template;
};

/**
* @param {!ITemplateArray} strings Constant parts of tagged template literal
* @param {!Array<*>} values Array of values from quasis
*/
const assertValidTemplateStringParameters = (strings, values) => {
// Note: if/when https://github.com/tc39/proposal-array-is-template-object
// is standardized, use that instead when available, as it can perform an
// unforgable check (though of course, the function itself can be forged).
if (!Array.isArray(strings) || !Array.isArray(strings.raw) ||
(values.length !== strings.length - 1)) {
// This is either caused by a browser bug, a compiler bug, or someone
// calling the html template tag function as a regular function.
//
throw new TypeError('Invalid call to the html template tag');
}
};

/**
* An html literal tag that can be used with `html` to compose.
* a literal string.
Expand All @@ -123,6 +174,5 @@ export const html = function html(strings, ...values) {
* @return {!LiteralString} Constructed literal string
*/
export const htmlLiteral = function(strings, ...values) {
return new LiteralString(values.reduce((acc, v, idx) =>
acc + literalValue(v) + strings[idx + 1], strings[0]));
return new LiteralString(strings, values);
};

0 comments on commit 10220c9

Please sign in to comment.