UNPKG

trusted-types

Version:

Polyfill for the Trusted Types

1,701 lines (1,555 loc) 62.4 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); /** * @license * Copyright 2017 Google Inc. All Rights Reserved. * * Licensed under the W3C SOFTWARE AND DOCUMENT NOTICE AND LICENSE. * * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document */ // TODO check attributes in safari, remove legacy safe handlers const unsafeAttributeEventHandlers = [ 'onabort', 'onactivate', 'onactivateinvisible', 'onafterprint', 'onafterupdate', 'onanimationcancel', 'onanimationend', 'onanimationiteration', 'onanimationstart', 'onariarequest', 'onauxclick', 'onbeforeactivate', 'onbeforecopy', 'onbeforecut', 'onbeforedeactivate', 'onbeforeeditfocus', 'onbeforepaste', 'onbeforeprint', 'onbeforeunload', 'onbegin', 'onblur', 'onbounce', 'oncancel', 'oncanplay', 'oncanplaythrough', 'oncellchange', 'onchange', 'onclick', 'onclose', 'oncommand', 'oncontextmenu', 'oncontrolselect', 'oncopy', 'oncuechange', 'oncut', 'ondataavailable', 'ondatasetchanged', 'ondatasetcomplete', 'ondblclick', 'ondeactivate', 'ondrag', 'ondragdrop', 'ondragend', 'ondragenter', 'ondragexit', 'ondragleave', 'ondragover', 'ondragstart', 'ondrop', 'ondurationchange', 'onemptied', 'onend', 'onended', 'onerror', 'onerrorupdate', 'onexit', 'onfilterchange', 'onfinish', 'onfocus', 'onfocusin', 'onfocusout', 'onformdata', 'onfullscreenchange', 'onfullscreenerror', 'ongotpointercapture', 'onhelp', 'oninput', 'oninvalid', 'onkeydown', 'onkeypress', 'onkeyup', 'onlayoutcomplete', 'onload', 'onloadeddata', 'onloadedmetadata', 'onloadend', 'onloadstart', 'onlosecapture', 'onlostpointercapture', 'onmediacomplete', 'onmediaerror', 'onmessage', 'onmousedown', 'onmouseenter', 'onmouseleave', 'onmousemove', 'onmouseout', 'onmouseover', 'onmouseup', 'onmousewheel', 'onmove', 'onmoveend', 'onmovestart', 'onmozfullscreenchange', 'onmozfullscreenerror', 'onmscontentzoom', 'onmsgesturechange', 'onmsgesturedoubletap', 'onmsgestureend', 'onmsgesturehold', 'onmsgesturestart', 'onmsgesturetap', 'onmsgotpointercapture', 'onmsinertiastart', 'onmslostpointercapture', 'onmsmanipulationstatechanged', 'onmspointercancel', 'onmspointerdown', 'onmspointerenter', 'onmspointerleave', 'onmspointermove', 'onmspointerout', 'onmspointerover', 'onmspointerup', 'onoffline', 'ononline', 'onoutofsync', 'onoverscroll', 'onpaste', 'onpause', 'onplay', 'onplaying', 'onpointercancel', 'onpointerdown', 'onpointerenter', 'onpointerleave', 'onpointermove', 'onpointerout', 'onpointerover', 'onpointerrawupdate', 'onpointerup', 'onprogress', 'onpropertychange', 'onratechange', 'onreadystatechange', 'onrepeat', 'onreset', 'onresize', 'onresizeend', 'onresizestart', 'onresume', 'onreverse', 'onrowdelete', 'onrowenter', 'onrowexit', 'onrowinserted', 'onscroll', 'onscrollend', 'onsearch', 'onseek', 'onseeked', 'onseeking', 'onselect', 'onselectionchange', 'onselectstart', 'onshow', 'onstalled', 'onstart', 'onstop', 'onstorage', 'onsubmit', 'onsuspend', 'onsynchrestored', 'ontimeerror', 'ontimeupdate', 'ontoggle', 'ontrackchange', 'ontransitioncancel', 'ontransitionend', 'ontransitionrun', 'ontransitionstart', 'onunload', 'onurlflip', 'onvolumechange', 'onwaiting', 'onwebkitanimationend', 'onwebkitanimationiteration', 'onwebkitanimationstart', 'onwebkitfullscreenchange', 'onwebkitfullscreenerror', 'onwebkittransitionend', 'onwheel', ]; /** * Returns an array of all event handlers. Some of the event handlers may be * supported only in some browsers. * * For every event type that the browser supports, SVG supports that as an event * attribute, following the same requirements as for HTML event attributes. * https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/Events * * @return {!Array<string>} */ function getUnsafeAttributeEventHandlers() { if (typeof window !== 'undefined') { const eventHandlers = []; for (const name in HTMLElement.prototype) { if (name.slice(0, 2) === 'on') { eventHandlers.push(name); } } return eventHandlers; } else { return unsafeAttributeEventHandlers; } } /** * @license * Copyright 2017 Google Inc. All Rights Reserved. * * Licensed under the W3C SOFTWARE AND DOCUMENT NOTICE AND LICENSE. * * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document */ const isBrowser = typeof window !== 'undefined'; const rejectInputFn = (s) => { throw new TypeError('undefined conversion'); }; const rejectInputDefaultPolicyFn = (s) => null; const {toLowerCase, toUpperCase} = String.prototype; const HTML_NS = 'http://www.w3.org/1999/xhtml'; const SVG_NS = 'http://www.w3.org/2000/svg'; /** * @constructor * @property {!function(string):TrustedHTML} createHTML * @property {!function(string):TrustedScriptURL} createScriptURL * @property {!function(string):TrustedScript} createScript * @property {!string} name */ const TrustedTypePolicy = function() { throw new TypeError('Illegal constructor'); }; /** * @constructor */ const TrustedTypePolicyFactory = function() { throw new TypeError('Illegal constructor'); }; /* eslint-enable no-unused-vars */ const DEFAULT_POLICY_NAME = 'default'; const trustedTypesBuilderTestOnly = function() { // Capture common names early. const { assign, create, defineProperty, freeze, getOwnPropertyNames, getPrototypeOf, prototype: ObjectPrototype, } = Object; const {hasOwnProperty} = ObjectPrototype; const { forEach, push, } = Array.prototype; const creatorSymbol = Symbol(); /** * Getter for the privateMap. * @param {Object} obj Key of the privateMap * @return {Object<string, string>} Private storage. */ const privates = function(obj) { let v = privateMap.get(obj); if (v === undefined) { v = create(null); // initialize the private storage. privateMap.set(obj, v); } return v; }; /** * Called before attacker-controlled code on an internal collections, * copies prototype members onto the instance directly, so that later * changes to prototypes cannot expose collection internals. * @param {!T} collection * @return {!T} collection * @template T */ function selfContained(collection) { const proto = getPrototypeOf(collection); if (proto == null || getPrototypeOf(proto) !== ObjectPrototype) { throw new Error(); // Loop below is insufficient. } for (const key of getOwnPropertyNames(proto)) { defineProperty(collection, key, {value: collection[key]}); } return collection; } /** * Map for private properties of Trusted Types object. * This is so that the access to the type constructor does not give * the ability to create typed values. * @type {WeakMap} */ const privateMap = selfContained(new WeakMap()); /** * List of all configured policy names. * @type {Array<string>} */ const policyNames = selfContained([]); /** * Allowed policy names. * Applied only if enforceNameRestrictions is true. * @type {Array<string>} */ const allowedNames = selfContained([]); /** * Should duplicate prolicy names be allowed. * Applied only if enforceNameRestrictions is true. * @type {boolean} */ let allowDuplicateNames = true; /** * A reference to a default policy, if created. * @type {TrustedTypePolicy} */ let defaultPolicy = null; /** * Whether to enforce allowedNames in createPolicy(). * @type {boolean} */ let enforceNameRestrictions = false; /** * A value that is trusted to have certain security-relevant properties * because the sources of such values are controlled. */ class TrustedType { /** * Constructor for TrustedType. Only allowed to be called from within a * policy. * @param {symbol} s creatorSymbol * @param {string} policyName The name of the policy this object was * created by. */ constructor(s, policyName) { // TODO: Figure out if symbol is needed, if the value is in privateMap. if (s !== creatorSymbol) { throw new Error('cannot call the constructor'); } defineProperty(this, 'policyName', {value: '' + policyName, enumerable: true}); } /** * Returns the wrapped string value of the object. * @return {string} * @override */ toString() { return privates(this)['v']; } /** * Returns the wrapped string value of the object. * @return {string} * @override */ valueOf() { return privates(this)['v']; } } /** * @param {function(new:TrustedType, symbol, string)} SubClass * @param {string} canonName The class name which should be independent of * any renaming pass and which is relied upon by the enforcer and for * native type interop. */ function lockdownTrustedType(SubClass, canonName) { freeze(SubClass.prototype); delete SubClass.name; defineProperty(SubClass, 'name', {value: canonName}); } /** * Trusted Script URL object wrapping a string that can only be created from a * TT policy. */ class TrustedScriptURL extends TrustedType { } lockdownTrustedType(TrustedScriptURL, 'TrustedScriptURL'); /** * Trusted HTML object wrapping a string that can only be created from a * TT policy. */ class TrustedHTML extends TrustedType { } lockdownTrustedType(TrustedHTML, 'TrustedHTML'); /** * Trusted Script object wrapping a string that can only be created from a * TT policy. */ class TrustedScript extends TrustedType { } lockdownTrustedType(TrustedScript, 'TrustedScript'); lockdownTrustedType(TrustedType, 'TrustedType'); // Common constants. const emptyHTML = freeze(create(new TrustedHTML(creatorSymbol, ''))); privates(emptyHTML)['v'] = ''; const emptyScript = freeze(create(new TrustedScript(creatorSymbol, ''))); privates(emptyScript)['v'] = ''; /** * A map of attribute / property names to allowed types * for known namespaces. * @type {!Object<string,!TrustedTypesTypeMap>} * @export */ const TYPE_MAP = { [HTML_NS]: { // TODO(koto): Figure out what to to with <link> 'EMBED': { 'attributes': { 'src': TrustedScriptURL.name, }, }, 'IFRAME': { 'attributes': { 'srcdoc': TrustedHTML.name, }, }, 'OBJECT': { 'attributes': { 'data': TrustedScriptURL.name, 'codebase': TrustedScriptURL.name, }, }, // TODO(koto): Figure out what to do with portals. 'SCRIPT': { 'attributes': { 'src': TrustedScriptURL.name, 'text': TrustedScript.name, }, 'properties': { 'innerText': TrustedScript.name, 'textContent': TrustedScript.name, 'text': TrustedScript.name, }, }, '*': { 'attributes': {}, 'properties': { 'innerHTML': TrustedHTML.name, 'outerHTML': TrustedHTML.name, }, }, }, [SVG_NS]: { '*': { 'attributes': {}, 'properties': {}, }, }, }; /** * A map of element property to HTML attribute names. * @type {!Object<string, string>} */ const ATTR_PROPERTY_MAP = { 'codebase': 'codeBase', 'formaction': 'formAction', }; // Edge doesn't support srcdoc. if (isBrowser && !('srcdoc' in HTMLIFrameElement.prototype)) { delete TYPE_MAP[HTML_NS]['IFRAME']['attributes']['srcdoc']; } // in HTML, clone attributes into properties. for (const tag of Object.keys(TYPE_MAP[HTML_NS])) { if (!TYPE_MAP[HTML_NS][tag]['properties']) { TYPE_MAP[HTML_NS][tag]['properties'] = {}; } for (const attr of Object.keys(TYPE_MAP[HTML_NS][tag]['attributes'])) { TYPE_MAP[HTML_NS][tag]['properties'][ ATTR_PROPERTY_MAP[attr] ? ATTR_PROPERTY_MAP[attr] : attr ] = TYPE_MAP[HTML_NS][tag]['attributes'][attr]; } } // Add inline event handlers attribute names. for (const name of getUnsafeAttributeEventHandlers()) { TYPE_MAP[HTML_NS]['*']['attributes'][name] = 'TrustedScript'; TYPE_MAP[SVG_NS]['*']['attributes'][name] = 'TrustedScript'; } /** * @type {!Object<string,!Function>} */ const createTypeMapping = { 'createHTML': TrustedHTML, 'createScriptURL': TrustedScriptURL, 'createScript': TrustedScript, }; const createFunctionAllowed = createTypeMapping.hasOwnProperty; /** * Function generating a type checker. * @template T * @param {T} type The type to check against. * @return {function(*):boolean} */ function isTrustedTypeChecker(type) { return (obj) => (obj instanceof type) && privateMap.has(obj); } /** * Wraps a user-defined policy rules with TT constructor * @param {string} policyName The policy name * @param {TrustedTypesInnerPolicy} innerPolicy InnerPolicy * @return {!TrustedTypePolicy} Frozen policy object */ function wrapPolicy(policyName, innerPolicy) { /** * @template T * @param {function(new:T, symbol, string)} Ctor a trusted type constructor * @param {string} methodName the policy factory method name * @return {function(string):!T} a factory that produces instances of Ctor. */ function creator(Ctor, methodName) { // This causes thisValue to be null when called below. const method = innerPolicy[methodName] || ( policyName == DEFAULT_POLICY_NAME ? rejectInputDefaultPolicyFn : rejectInputFn ); const policySpecificType = freeze(new Ctor(creatorSymbol, policyName)); const factory = { [methodName](s, ...args) { // Trick to get methodName to show in stacktrace. let result = method('' + s, ...args); if (result === undefined || result === null) { if (policyName == DEFAULT_POLICY_NAME) { // These values mean that the input was rejected. This will cause // a violation later, don't create types for them. return result; } result = ''; } const allowedValue = '' + result; const o = freeze(create(policySpecificType)); privates(o)['v'] = allowedValue; return o; }, }[methodName]; return freeze(factory); } const policy = create(TrustedTypePolicy.prototype); for (const name of getOwnPropertyNames(createTypeMapping)) { policy[name] = creator(createTypeMapping[name], name); } defineProperty(policy, 'name', { value: policyName, writable: false, configurable: false, enumerable: true, }); return /** @type {!TrustedTypePolicy} */ (freeze(policy)); } /** * Returns the name of the trusted type required for a given element * attribute. * @param {string} tagName The name of the tag of the element. * @param {string} attribute The name of the attribute. * @param {string=} elementNs Element namespace. * @param {string=} attributeNs The attribute namespace. * @return {string?} Required type name or null, if a Trusted * Type is not required. */ function getAttributeType(tagName, attribute, elementNs = '', attributeNs = '') { const canonicalAttr = toLowerCase.apply(String(attribute)); return getTypeInternal_(tagName, 'attributes', canonicalAttr, elementNs, attributeNs) || null; } /** * Returns a type name from a type map. * @param {string} tag A tag name. * @param {string} container 'attributes' or 'properties' * @param {string} name The attribute / property name. * @param {string=} elNs Element namespace. * @param {string=} attrNs Attribute namespace. * @return {string|undefined} * @private */ function getTypeInternal_(tag, container, name, elNs = '', attrNs = '') { const canonicalTag = toUpperCase.apply(String(tag)); let ns = attrNs ? attrNs : elNs; if (!ns) { ns = HTML_NS; } const map = hasOwnProperty.apply(TYPE_MAP, [ns]) ? TYPE_MAP[ns] : null; if (!map) { return; } if (hasOwnProperty.apply(map, [canonicalTag]) && map[canonicalTag] && hasOwnProperty.apply(map[canonicalTag][container], [name]) && map[canonicalTag][container][name]) { return map[canonicalTag][container][name]; } if (hasOwnProperty.apply(map, ['*']) && hasOwnProperty.apply(map['*'][container], [name]) && map['*'][container][name]) { return map['*'][container][name]; } } /** * Returns the name of the trusted type required for a given element property. * @param {string} tagName The name of the tag of the element. * @param {string} property The property. * @param {string=} elementNs Element namespace. * @return {string?} Required type name or null, if a Trusted * Type is not required. */ function getPropertyType(tagName, property, elementNs = '') { // TODO: Support namespaces. return getTypeInternal_( tagName, 'properties', String(property), elementNs) || null; } /** * Returns the type map-like object, that resolves a name of a type for a * given tag + attribute / property in a given namespace. * The keys of the map are uppercase tag names. Map entry has mappings between * a lowercase attribute name / case-sensitive property name and a name of the * type that is required for that attribute / property. * Example entry for 'IMG': {"attributes": {"src": "TrustedHTML"}} * @param {string=} namespaceUri The namespace URI (will use the current * document namespace URI if omitted). * @return {TrustedTypesTypeMap} */ function getTypeMapping(namespaceUri = '') { if (!namespaceUri) { try { namespaceUri = document.documentElement.namespaceURI; } catch (e) { namespaceUri = HTML_NS; } } /** * @template T * @private * @param {T} o * @return {T} */ function deepClone(o) { return JSON.parse(JSON.stringify(o)); } const map = TYPE_MAP[namespaceUri]; if (!map) { return {}; } return deepClone(map); } /** * Creates a TT policy. * * Returns a frozen object representing a policy - a collection of functions * that may create TT objects based on the user-provided rules specified * in the policy object. * * @param {string} name A unique name of the policy. * @param {TrustedTypesInnerPolicy} policy Policy rules object. * @return {TrustedTypePolicy} The policy that may create TT objects * according to the policy rules. */ function createPolicy(name, policy) { const pName = '' + name; // Assert it's a string if (!pName.match(/^[-#a-zA-Z0-9=_/@.%]+$/g)) { throw new TypeError('Policy ' + pName + ' contains invalid characters.'); } if (enforceNameRestrictions && allowedNames.indexOf(pName) === -1 && allowedNames.indexOf('*') === -1) { throw new TypeError('Policy ' + pName + ' disallowed.'); } if (pName === DEFAULT_POLICY_NAME && defaultPolicy) { throw new TypeError('Policy ' + pName + ' already exists.'); } if (enforceNameRestrictions && !allowDuplicateNames && policyNames.indexOf(pName) !== -1) { throw new TypeError('Policy ' + pName + ' exists.'); } // Register the name early so that if policy getters unwisely calls // across protection domains to code that reenters this function, // policy author still has rights to the name. policyNames.push(pName); // Only copy own properties of names present in createTypeMapping. const innerPolicy = create(null); if (policy && typeof policy === 'object') { // Treat non-objects as empty policies. for (const key of getOwnPropertyNames(policy)) { if (createFunctionAllowed.call(createTypeMapping, key)) { innerPolicy[key] = policy[key]; } } } else { // eslint-disable-next-line no-console console.warn('trustedTypes.createPolicy ' + pName + ' was given an empty policy'); } freeze(innerPolicy); const wrappedPolicy = wrapPolicy(pName, innerPolicy); if (pName === DEFAULT_POLICY_NAME) { defaultPolicy = wrappedPolicy; } return wrappedPolicy; } /** * Applies the policy name restrictions. * @param {!Array<string>} allowedPolicyNames * @param {boolean} allowDuplicates */ function setPolicyNameRestrictions(allowedPolicyNames, allowDuplicates) { enforceNameRestrictions = true; allowedNames.length = 0; forEach.call(allowedPolicyNames, (el) => { push.call(allowedNames, '' + el); }); allowDuplicateNames = allowDuplicates; policyNames.length = 0; // Clear already used policy names list. } /** * Clears the policy name restrictions. */ function clearPolicyNameRestrictions() { enforceNameRestrictions = false; } /** * Returns the default policy, or null if it was not created. * @return {TrustedTypePolicy} */ function getDefaultPolicy() { return defaultPolicy; } /** * Resets the default policy. */ function resetDefaultPolicy() { defaultPolicy = null; policyNames.splice(policyNames.indexOf(DEFAULT_POLICY_NAME), 1); } const api = create(TrustedTypePolicyFactory.prototype); assign(api, { // The main function to create policies. createPolicy, // Type checkers, also validating the object was initialized through a // policy. isHTML: isTrustedTypeChecker(TrustedHTML), isScriptURL: isTrustedTypeChecker(TrustedScriptURL), isScript: isTrustedTypeChecker(TrustedScript), getAttributeType, getPropertyType, getTypeMapping, emptyHTML, emptyScript, defaultPolicy, // Just to make the compiler happy, this is overridden below. TrustedHTML: TrustedHTML, TrustedScriptURL: TrustedScriptURL, TrustedScript: TrustedScript, }); defineProperty(api, 'defaultPolicy', { get: getDefaultPolicy, set: () => {}, }); return { trustedTypes: freeze(api), setPolicyNameRestrictions, clearPolicyNameRestrictions, getDefaultPolicy, resetDefaultPolicy, }; }; const { trustedTypes, setPolicyNameRestrictions, clearPolicyNameRestrictions, getDefaultPolicy, resetDefaultPolicy, } = trustedTypesBuilderTestOnly(); /** * @license * Copyright 2017 Google Inc. All Rights Reserved. * * Licensed under the W3C SOFTWARE AND DOCUMENT NOTICE AND LICENSE. * * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document */ /** * CSP Directive name controlling Trusted Types behavior. * @type {string} */ const ENFORCEMENT_DIRECTIVE_NAME = 'require-trusted-types-for'; const POLICIES_DIRECTIVE_NAME = 'trusted-types'; /** * A configuration object for trusted type enforcement. */ class TrustedTypeConfig { /** * @param {boolean} isLoggingEnabled If true enforcement wrappers will log * violations to the console. * @param {boolean} isEnforcementEnabled If true enforcement is enabled at * runtime. * @param {Array<string>} allowedPolicyNames Whitelisted policy names. * @param {boolean} allowDuplicates Should duplicate names be allowed. * @param {?string} cspString String with the CSP policy. * @param {?Window} windowObject to monkey patch */ constructor(isLoggingEnabled, isEnforcementEnabled, allowedPolicyNames, allowDuplicates, cspString = null, windowObject = null) { /** * True if logging is enabled. * @type {boolean} */ this.isLoggingEnabled = isLoggingEnabled; /** * True if enforcement is enabled. * @type {boolean} */ this.isEnforcementEnabled = isEnforcementEnabled; /** * Allowed policy names. * @type {Array<string>} */ this.allowedPolicyNames = allowedPolicyNames; /** * Should duplicate names be accepted. * @type {boolean} */ this.allowDuplicates = allowDuplicates; /** * CSP string that defined the policy. * @type {?string} */ this.cspString = cspString; /** * Window like object to monkey patch. * @type {?Window} */ this.windowObject = windowObject; } /** * Parses a CSP policy. * @link https://www.w3.org/TR/CSP3/#parse-serialized-policy * @param {string} cspString String with a CSP definition. * @return {Object<string,Array<string>>} Parsed CSP, keyed by directive * names. */ static parseCSP(cspString) { const SEMICOLON = /\s*;\s*/; const WHITESPACE = /\s+/; return cspString.trim().split(SEMICOLON) .map((serializedDirective) => serializedDirective.split(WHITESPACE)) .reduce(function(parsed, directive) { if (directive[0]) { parsed[directive[0]] = directive.slice(1).map((s) => s).sort(); } return parsed; }, {}); } /** * Creates a TrustedTypeConfig object from a CSP string. * @param {string} cspString * @return {!TrustedTypeConfig} */ static fromCSP(cspString) { const isLoggingEnabled = true; const policy = TrustedTypeConfig.parseCSP(cspString); const enforce = ENFORCEMENT_DIRECTIVE_NAME in policy && policy[ENFORCEMENT_DIRECTIVE_NAME].includes('\'script\''); let policies = ['*']; let allowDuplicates = true; if (POLICIES_DIRECTIVE_NAME in policy) { policies = policy[POLICIES_DIRECTIVE_NAME].filter( (p) => p.charAt(0) !== '\''); allowDuplicates = policy[POLICIES_DIRECTIVE_NAME].includes( '\'allow-duplicates\''); if (policy[POLICIES_DIRECTIVE_NAME].length == 1 && policy[POLICIES_DIRECTIVE_NAME][0] == '\'none\'') { policies = []; } } return new TrustedTypeConfig( isLoggingEnabled, enforce, /* isEnforcementEnabled */ policies, /* allowedPolicyNames */ allowDuplicates, /* allowDuplicates */ cspString ); } } /** * @license * Copyright 2017 Google Inc. All Rights Reserved. * * Licensed under the W3C SOFTWARE AND DOCUMENT NOTICE AND LICENSE. * * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document */ const { defineProperty, } = Object; /** * Installs the setter of a given property. * @param {!Object} object An object for which to wrap the property. * @param {string} name The name of the property to wrap. * @param {function(*): *|undefined} setter A setter function} */ function installSetter(object, name, setter) { const descriptor = { set: setter, }; defineProperty(object, name, descriptor); } /** * Installs a setter and getter of a given property. * @param {!Object} object An object for which to wrap the property. * @param {string} name The name of the property to wrap. * @param {function(*): *|undefined} setter A setter function} * @param {function(*): *|undefined} getter A getter function} */ function installSetterAndGetter(object, name, setter, getter) { const descriptor = { set: setter, get: getter, configurable: true, // This can get uninstalled, we need configurable: true }; defineProperty(object, name, descriptor); } /** * Installs the setter of a given property. * @param {!Object} object An object for which to wrap the property. * @param {string} name The name of the property to wrap. * @param {function(*): *|undefined} fn A function} */ function installFunction(object, name, fn) { defineProperty(object, name, { value: fn, }); } /** * @license * Copyright 2017 Google Inc. All Rights Reserved. * * Licensed under the W3C SOFTWARE AND DOCUMENT NOTICE AND LICENSE. * * https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document */ const {apply} = Reflect; const { getOwnPropertyNames, getOwnPropertyDescriptor, getPrototypeOf, } = Object; const { hasOwnProperty, isPrototypeOf, } = Object.prototype; const {slice} = String.prototype; let stringifyForRangeHack; /** * Parses URL, catching all the errors. * @param {string} url URL string to parse. * @param {Window} windowObject the window object. * @return {URL|null} */ function parseUrl_(url, windowObject) { // No URL in IE 11. const UrlConstructor = typeof windowObject.URL == 'function' ? windowObject.URL.prototype.constructor : null; try { return new UrlConstructor(url, windowObject.document.baseURI || undefined); } catch (e) { return null; } } // We don't actually need other namespaces. // setAttribute is hooked on Element.prototype, which all elements inherit from, // and all sensitive property wrappers are hooked directly on Element as well. const typeMap = trustedTypes.getTypeMapping(HTML_NS); const STRING_TO_TYPE = { 'TrustedHTML': trustedTypes.TrustedHTML, 'TrustedScript': trustedTypes.TrustedScript, 'TrustedScriptURL': trustedTypes.TrustedScriptURL, }; for (const tagName of Object.keys(typeMap)) { const attrs = typeMap[tagName]['properties']; for (const [k, v] of Object.entries(attrs)) { attrs[k] = STRING_TO_TYPE[v]; } } /** * Map of type names to type checking function. * @type {!Object<string,!Function>} */ const TYPE_CHECKER_MAP = { 'TrustedHTML': trustedTypes.isHTML, 'TrustedScriptURL': trustedTypes.isScriptURL, 'TrustedScript': trustedTypes.isScript, }; /** * Map of type names to type producing function. * @type {Object<string,string>} */ const TYPE_PRODUCER_MAP = { 'TrustedHTML': 'createHTML', 'TrustedScriptURL': 'createScriptURL', 'TrustedScript': 'createScript', }; /* eslint-enable no-unused-vars */ /** * An object for enabling trusted type enforcement. */ class TrustedTypesEnforcer { /** * @param {!TrustedTypeConfig} config The configuration for */ constructor(config) { /** * A configuration for the trusted type enforcement. * @private {!TrustedTypeConfig} */ this.config_ = config; /** * @private {Object<string, function(*): *|undefined>} */ this.originalSetters_ = {}; /** * The object that will be monkey patched by the polyfill. * @private {Window} */ this.windowObject_ = config.windowObject || (typeof window !== 'undefined' ? window : null); if (!this.isValidWindowObject_()) { throw new Error( // eslint-disable-next-line 'The polyfill expects a global `window` object or emulated `window-like` object passed to the enforcer as second argument' ); } // In IE 11, insertAdjacent(HTML|Text) is on HTMLElement prototype this.insertAdjacentObjectPrototype = ((w) => { return apply(hasOwnProperty, w.Element.prototype, ['insertAdjacentHTML']) ? w.Element.prototype : w.HTMLElement.prototype; })(this.windowObject_); this.functionConstructorNameGetter = this.windowObject_.document.createElement('div').constructor.name ? (fn) => fn.name : (fn) => ('' + fn).match(/^\[object (\S+)\]$/)[1]; } /** * Validates that the windowObject is in correct form. * @return {boolean} */ isValidWindowObject_() { const w = this.windowObject_; const requiredSymbols = [ 'Element', 'HTMLElement', 'Document', 'Node', 'document', ]; return !!w && typeof w === 'object' && requiredSymbols.every((s) => s in w); } /** * Checks whether the value is instanceOf the specific window object. * @param {*} value * @param {string} winProp * @return {boolean} * @private */ instanceOfDomProperty(value, winProp) { const obj = this.windowObject_[winProp]; return !!obj && value instanceof obj; } /** * Converts an uppercase tag name to an element constructor function name. * Used for property setter hijacking only. * @param {string} tagName * @return {?string} */ convertTagToConstructor(tagName) { if (tagName == '*') { return 'HTMLElement'; } else { return this.getConstructorName_( this.windowObject_.document.createElement(tagName).constructor ); } } /** * Return object constructor name (their function.name is * not available in IE 11). * @param {Function} fn * @return {?string} * @private */ getConstructorName_(fn) { return this.functionConstructorNameGetter(fn); } /** * Wraps HTML sinks with an enforcement setter, which will enforce * trusted types and do logging, if enabled. * * Every HTML sink is feature tested for existance first and TT is * enforced only when it exists. This is becuase the polyfill can work * with emulated window-like objects, which might not be fully compatible * with browser DOM. */ install() { setPolicyNameRestrictions(this.config_.allowedPolicyNames, this.config_.allowDuplicates); if (!this.config_.isEnforcementEnabled && !this.config_.isLoggingEnabled) { return; } if ('ShadowRoot' in this.windowObject_) { this.wrapSetter_(this.windowObject_.ShadowRoot.prototype, 'innerHTML', trustedTypes.TrustedHTML); } stringifyForRangeHack = (function(doc) { if (!doc.createRange) return false; const r = doc.createRange(); // In IE 11 Range.createContextualFragment doesn't stringify its argument. const f = r.createContextualFragment(/** @type {string} */ ( {toString: () => '<div></div>'})); return f.childNodes.length == 0; })(this.windowObject_.document); if (this.windowObject_.Range) { this.wrapWithEnforceFunction_( this.windowObject_.Range.prototype, 'createContextualFragment', trustedTypes.TrustedHTML, 0); } if (this.insertAdjacentObjectPrototype) { this.wrapWithEnforceFunction_(this.insertAdjacentObjectPrototype, 'insertAdjacentHTML', trustedTypes.TrustedHTML, 1); } if (getOwnPropertyDescriptor( this.windowObject_.Document.prototype, 'write' )) { // Chrome this.wrapWithEnforceFunction_(this.windowObject_.Document.prototype, 'write', trustedTypes.TrustedHTML, 0); } else if (this.windowObject_.HTMLDocument && getOwnPropertyDescriptor( this.windowObject_.HTMLDocument.prototype, 'write')) { // Firefox this.wrapWithEnforceFunction_(this.windowObject_.HTMLDocument.prototype, 'write', trustedTypes.TrustedHTML, 0); } if ('DOMParser' in this.windowObject_) { this.wrapWithEnforceFunction_( this.windowObject_.DOMParser.prototype, 'parseFromString', trustedTypes.TrustedHTML, 0); } if (this.windowObject_.hasOwnProperty('setInterval')) { this.wrapWithEnforceFunction_(this.windowObject_, 'setInterval', trustedTypes.TrustedScript, 0); } if (this.windowObject_.hasOwnProperty('setTimeout')) { this.wrapWithEnforceFunction_(this.windowObject_, 'setTimeout', trustedTypes.TrustedScript, 0); } this.wrapSetAttribute_(); this.installScriptMutatorGuards_(); this.installPropertySetWrappers_(); } /** * Removes the original setters. */ uninstall() { clearPolicyNameRestrictions(); if (!this.config_.isEnforcementEnabled && !this.config_.isLoggingEnabled) { return; } if ('ShadowRoot' in this.windowObject_) { this.restoreSetter_(this.windowObject_.ShadowRoot.prototype, 'innerHTML'); } if (this.windowObject_.Range) { this.restoreFunction_( this.windowObject_.Range.prototype, 'createContextualFragment' ); } this.restoreFunction_( this.insertAdjacentObjectPrototype, 'insertAdjacentHTML' ); this.restoreFunction_( this.windowObject_.Element.prototype, 'setAttribute'); this.restoreFunction_( this.windowObject_.Element.prototype, 'setAttributeNS'); if (this.windowObject_.Document && getOwnPropertyDescriptor( this.windowObject_.Document.prototype, 'write') ) { this.restoreFunction_(this.windowObject_.Document.prototype, 'write'); } else if (this.windowObject_.HTMLDocument && getOwnPropertyDescriptor( this.windowObject_.HTMLDocument.prototype, 'write') ) { this.restoreFunction_(this.windowObject_.HTMLDocument.prototype, 'write'); } if ('DOMParser' in this.windowObject_) { this.restoreFunction_(DOMParser.prototype, 'parseFromString'); } if (this.windowObject_.hasOwnProperty('setTimeout')) { this.restoreFunction_(this.windowObject_, 'setTimeout'); } if (this.windowObject_.hasOwnProperty('setInterval')) { this.restoreFunction_(this.windowObject_, 'setInterval'); } this.uninstallPropertySetWrappers_(); this.uninstallScriptMutatorGuards_(); resetDefaultPolicy(); } /** * Installs type-enforcing wrappers for APIs that allow to modify * script element texts. * @private */ installScriptMutatorGuards_() { const that = this; ['appendChild', 'insertBefore', 'replaceChild'].forEach((fnName) => { this.wrapFunction_( this.windowObject_.Node.prototype, fnName, /** * @this {Node} * @param {function(!Function, ...*)} originalFn * @return {*} */ function(originalFn, ...args) { return that.enforceTypeInScriptNodes_ .bind(that, this, /* checkParent */ false, originalFn) .apply(that, args); }); }); if (this.insertAdjacentObjectPrototype) { this.wrapFunction_( this.insertAdjacentObjectPrototype, 'insertAdjacentText', /** * @this {Element} * @param {function(!Function, ...*)} originalFn * @return {*} */ function(originalFn, ...args) { return that.insertAdjacentTextWrapper_ .bind(that, this, originalFn) .apply(that, args); }); } ['after', 'before', 'replaceWith'].forEach((fnName) => { if (fnName in this.windowObject_.Element.prototype) { this.wrapFunction_( this.windowObject_.Element.prototype, fnName, /** * @this {Element} * @param {function(!Function, ...*)} originalFn * @return {*} */ function(originalFn, ...args) { return that.enforceTypeInScriptNodes_ .bind(that, this, /* checkParent */ true, originalFn) .apply(that, args); }); } }); ['append', 'prepend'].forEach((fnName) => { if (fnName in this.windowObject_.Element.prototype) { this.wrapFunction_( this.windowObject_.Element.prototype, fnName, /** * @this {Element} * @param {function(!Function, ...*)} originalFn * @return {*} */ function(originalFn, ...args) { return that.enforceTypeInScriptNodes_ .bind(that, this, /* checkParent */ false, originalFn) .apply(that, args); }); } }); } /** * Uninstalls type-enforcing wrappers for APIs that allow to modify * script element texts. * @private */ uninstallScriptMutatorGuards_() { this.restoreFunction_( this.windowObject_.Node.prototype, 'appendChild'); this.restoreFunction_( this.windowObject_.Node.prototype, 'insertBefore'); this.restoreFunction_( this.windowObject_.Node.prototype, 'replaceChild'); this.restoreFunction_( this.insertAdjacentObjectPrototype, 'insertAdjacentText'); ['after', 'before', 'replaceWith', 'append', 'prepend'].forEach( (fnName) => { if (fnName in this.windowObject_.Element.prototype) { this.restoreFunction_( this.windowObject_.Element.prototype, fnName ); } } ); } /** * Installs wrappers for directly setting properties * based on the type map. * @private */ installPropertySetWrappers_() { /* eslint-disable guard-for-in */ for (const tag of getOwnPropertyNames(typeMap)) { for (const property of getOwnPropertyNames(typeMap[tag]['properties'])) { const constr = this.convertTagToConstructor(tag); if (constr != null && this.windowObject_[constr]) { this.wrapSetter_( this.windowObject_[constr].prototype, property, typeMap[tag]['properties'][property]); } } } } /** * Uninstalls wrappers for directly setting properties * based on the type map. * @private */ uninstallPropertySetWrappers_() { /* eslint-disable guard-for-in */ for (const tag of getOwnPropertyNames(typeMap)) { for (const property of getOwnPropertyNames(typeMap[tag]['properties'])) { const constr = this.convertTagToConstructor(tag); if (constr != null && this.windowObject_[constr]) { this.restoreSetter_( this.windowObject_[constr].prototype, property); } } } } /** Wraps set attribute with an enforcement function. */ wrapSetAttribute_() { const that = this; this.wrapFunction_( this.windowObject_.Element.prototype, 'setAttribute', /** * @this {TrustedTypesEnforcer} * @param {function(!Function, ...*)} originalFn * @return {*} */ function(originalFn, ...args) { return that.setAttributeWrapper_ .bind(that, this, originalFn) .apply(that, args); }); this.wrapFunction_( this.windowObject_.Element.prototype, 'setAttributeNS', /** * @this {TrustedTypesEnforcer} * @param {function(!Function, ...*)} originalFn * @return {*} */ function(originalFn, ...args) { return that.setAttributeNSWrapper_ .bind(that, this, originalFn) .apply(that, args); }); } /** * Enforces type checking for Element.prototype.setAttribute. * @param {!Object} context The context for the call to the original function. * @param {!Function} originalFn The original setAttribute function. * @return {*} */ setAttributeWrapper_(context, originalFn, ...args) { // Note(slekies): In a normal application constructor should never be null. // However, there are no guarantees. If the constructor is null, we cannot // determine whether a special type is required. In order to not break the // application, we will not do any further type checks and pass the call // to setAttribute. if (context.constructor !== null && this.instanceOfDomProperty(context, 'Element')) { const attrName = (args[0] = String(args[0])).toLowerCase(); const requiredType = trustedTypes.getAttributeType(context['tagName'], attrName, context['namespaceURI']); if (requiredType && apply(hasOwnProperty, STRING_TO_TYPE, [requiredType])) { return this.enforce_( context, 'setAttribute', STRING_TO_TYPE[requiredType], originalFn, 1, args); } } return apply(originalFn, context, args); } /** * Enforces type checking for Element.prototype.setAttributeNS. * @param {!Object} context The context for the call to the original function. * @param {!Function} originalFn The original setAttributeNS function. * @return {*} */ setAttributeNSWrapper_(context, originalFn, ...args) { // See the note from setAttributeWrapper_ above. if (context.constructor !== null && this.instanceOfDomProperty(context, 'Element')) { const ns = args[0] ? String(args[0]) : null; args[0] = ns; const attrName = (args[1] = String(args[1])).toLowerCase(); const requiredType = trustedTypes.getAttributeType(context['tagName'], attrName, context['namespaceURI'], ns); if (requiredType && apply(hasOwnProperty, STRING_TO_TYPE, [requiredType])) { return this.enforce_(context, 'setAttributeNS', STRING_TO_TYPE[requiredType], originalFn, 2, args); } } return apply(originalFn, context, args); } /** * Wrapper for DOM mutator functions that enforces type checks if the context * (or, optionally, its parent node) is a script node. * For each argument, it will make sure that text nodes pass through a * default policy, or generate a violation. To skip that check, pass * TrustedScript objects instead. * @param {!Element|!Node} context The context for the call to the original * function. * @param {boolean} checkParent Check parent of context instead. * @param {!Function} originalFn The original mutator function. * @return {*} */ enforceTypeInScriptNodes_(context, checkParent, originalFn, ...args) { const objToCheck = checkParent ? context.parentNode : context; if (this.instanceOfDomProperty(objToCheck, 'HTMLScriptElement') && args.length > 0) { for (let argNumber = 0; argNumber < args.length; argNumber++) { let arg = args[argNumber]; if (this.instanceOfDomProperty(arg, 'Node') && arg.nodeType !== this.windowObject_.Node.TEXT_NODE) { continue; // Type is not interesting } if (this.instanceOfDomProperty(arg, 'Node') && arg.nodeType == this.windowObject_.Node.TEXT_NODE) { arg = arg.textContent; } else if (trustedTypes.isScript(arg)) { // TODO(koto): Consider removing this branch, as it's hard to spec. // Convert to text node and go on. args[argNumber] = this.windowObject_.document.createTextNode('' + arg); continue; } // Try to run a default policy on argsthe argument const fallbackValue = this.maybeCallDefaultPolicy_( 'TrustedScript', '' + arg, 'script.text'); if (fallbackValue === null || fallbackValue === undefined) { this.processViolation_(context, originalFn.name, trustedTypes.TrustedScript, arg); } else { arg = fallbackValue; } args[argNumber] = this.windowObject_.document.createTextNode('' + arg); } } return apply(originalFn, context, args); } /** * Wrapper for Element.insertAdjacentText that enforces type checks for * inserting text into a script node. * @param {!Object} context The context for the call to the original function. * @param {!Function} originalFn The original insertAdjacentText function. */ insertAdjacentTextWrapper_(context, originalFn, ...args) { const riskyPositions = ['beforebegin', 'afterend']; if (this.instanceOfDomProperty(context, 'Element') && this.instanceOfDomProperty( context['parentElement'], 'HTMLScriptElement' ) && args.length > 1 && riskyPositions.includes(args[0]) && !(trustedTypes.isScript(args[1]))) { // Run a default policy on args[1] args[1] = '' + args[1]; const fallbackValue = this.maybeCallDefaultPolicy_('TrustedScript', args[1], 'script.text'); if (fallbackValue === null || fallbackValue === undefined) { this.processViolation_(context, 'insertAdjacentText', trustedTypes.TrustedScript, args[1]); } else { args[1] = fallbackValue; } const textNode = this.windowObject_.document.createTextNode('' + args[1]); const insertBefore = /** @type function(this: Node) */( this.originalSetters_[this.getKey_( this.windowObject_.Node.prototype, 'insertBefore' )]); switch (args[0]) { case riskyPositions[0]: // 'beforebegin' apply(insertBefore, context['parentElement'], [textNode, context]); break; case riskyPositions[1]: // 'afterend' apply(insertBefore, context['parentElement'], [textNode, context['nextSibling']]); break; } return; } apply(originalFn, context, args); } /** * Wraps a setter with the enforcement wrapper. * @param {!Object} object The object of the to-be-wrapped property. * @param {string} name The name of the property. * @param {!Function} type The type to enforce. * @param {number} argNumber Number of the argument to enforce the type of. * @private */ wrapWithEnforceFunction_(object, name, type, argNumber) { const that = this; this.wrapFunction_( object, name, /** * @this {TrustedTypesEnforcer} * @param {function(!Function, ...*)} originalFn * @return {*} */ function(originalFn, ...args) { return that.enforce_.call(that, this, name, type, originalFn, argNumber, args); }); } /** * Wraps an existing function with a given function body and stores the * original function. * @param {!Object} object The object of the to-be-wrapped property. * @param {string} name The name of the property. * @param {function(!Function, ...*)} functionBody The wrapper function. */ wrapFunction_(object, name, functionBody) { const descriptor = getOwnPropertyDescriptor(object, name); const originalFn = /** @type function(*):* */ ( descriptor ? descriptor.value : null); if (!(