trusted-types
Version:
Polyfill for the Trusted Types
1,699 lines (1,555 loc) • 62.2 kB
JavaScript
/**
* @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 (!(originalFn instanceof Function)) {
throw new TypeError(
'Proper