diff --git a/package.json b/package.json index bdc9991183a4..02f59da54aa0 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "@types/semver": "^6.0.2", "@types/shelljs": "^0.8.6", "@types/systemjs": "0.19.32", + "@types/trusted-types": "^1.0.6", "@types/yaml": "^1.9.7", "@types/yargs": "^15.0.5", "@webcomponents/custom-elements": "^1.1.0", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index ff8c68c4fa6b..5ad13618a512 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -14,6 +14,7 @@ ts_library( deps = [ "//packages/zone.js/lib:zone_d_ts", "@npm//@types/hammerjs", + "@npm//@types/trusted-types", ], ) diff --git a/packages/animations/browser/src/render/css_keyframes/css_keyframes_driver.ts b/packages/animations/browser/src/render/css_keyframes/css_keyframes_driver.ts index 9f5b771b44fd..68805f196e2b 100644 --- a/packages/animations/browser/src/render/css_keyframes/css_keyframes_driver.ts +++ b/packages/animations/browser/src/render/css_keyframes/css_keyframes_driver.ts @@ -72,7 +72,7 @@ export class CssKeyframesDriver implements AnimationDriver { keyframeStr += `}\n`; const kfElm = document.createElement('style'); - kfElm.innerHTML = keyframeStr; + kfElm.textContent = keyframeStr; return kfElm; } diff --git a/packages/compiler/BUILD.bazel b/packages/compiler/BUILD.bazel index fa2e1a342d17..30837c5c86ae 100644 --- a/packages/compiler/BUILD.bazel +++ b/packages/compiler/BUILD.bazel @@ -10,6 +10,9 @@ ts_library( "src/**/*.ts", ], ), + deps = [ + "//packages:types", + ] ) ng_package( diff --git a/packages/compiler/src/output/jit_trusted_types.ts b/packages/compiler/src/output/jit_trusted_types.ts new file mode 100644 index 000000000000..791aafcfe42d --- /dev/null +++ b/packages/compiler/src/output/jit_trusted_types.ts @@ -0,0 +1,60 @@ + +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/** + * The Trusted Types policy used by Angular, or null if Trusted Types are not + * enabled/supported, or undefined if the policy has not been created yet. + */ +let trustedTypesPolicyForJit: TrustedTypePolicy|null|undefined; + +/** + * Returns the Trusted Types policy used by Angular, or null if Trusted Types + * are not enabled/supported. The first call to this function will create the + * policy. + */ +function getTrustedTypesPolicyForJit(): TrustedTypePolicy|null { + if (trustedTypesPolicyForJit === undefined) { + trustedTypesPolicyForJit = null; + if (typeof window !== 'undefined') { + try { + trustedTypesPolicyForJit = window.trustedTypes?.createPolicy('angular#jit', { + createHTML: (s: string) => s, + createScript: (s: string) => s, + createScriptURL: (s: string) => s + }) ?? + null; + } catch (e) { + // trustedTypes.createPolicy throws if called with a name that is already + // registered, even in report-only mode. Until the API changes, catch the + // error not to break the applications functionally. In such case, the + // code will fall back to using strings. + console.log(e); + } + } + } + return trustedTypesPolicyForJit; +} + +export function trustedFunctionForJit(...args: string[]): Function { + // return new Function(...args.map((a) => { + // return (getTrustedTypesPolicyForJit()?.createScript(a) ?? a) as string; + // })); + + // Workaround for a Trusted Type bug in Chrome 83. Use the above when the Angular test suite uses + // Chrome 84. + const fnArgs = args.slice(0, -1).join(','); + const fnBody = args.pop()!.toString(); + const body = `(function anonymous(${fnArgs} +) { ${fnBody} +}).bind(globalThis)`; + const res = + eval((getTrustedTypesPolicyForJit()?.createScript(body) ?? body) as string) as Function; + res.toString = () => body; // To fix sourcemaps + return res as Function; +} diff --git a/packages/compiler/src/output/output_jit.ts b/packages/compiler/src/output/output_jit.ts index 75be4da28053..f89b9a63c6a1 100644 --- a/packages/compiler/src/output/output_jit.ts +++ b/packages/compiler/src/output/output_jit.ts @@ -11,6 +11,7 @@ import {CompileReflector} from '../compile_reflector'; import {EmitterVisitorContext} from './abstract_emitter'; import {AbstractJsEmitterVisitor} from './abstract_js_emitter'; +import {trustedFunctionForJit} from './jit_trusted_types'; import * as o from './output_ast'; /** @@ -69,11 +70,11 @@ export class JitEvaluator { // function anonymous(a,b,c // /**/) { ... }``` // We don't want to hard code this fact, so we auto detect it via an empty function first. - const emptyFn = new Function(...fnArgNames.concat('return null;')).toString(); + const emptyFn = trustedFunctionForJit(...fnArgNames.concat('return null;')).toString(); const headerLines = emptyFn.slice(0, emptyFn.indexOf('return null;')).split('\n').length - 1; fnBody += `\n${ctx.toSourceMapGenerator(sourceUrl, headerLines).toJsComment()}`; } - const fn = new Function(...fnArgNames.concat(fnBody)); + const fn = trustedFunctionForJit(...fnArgNames.concat(fnBody)); return this.executeFunction(fn, fnArgValues); } diff --git a/packages/compiler/src/render3/r3_identifiers.ts b/packages/compiler/src/render3/r3_identifiers.ts index 3ea863e09d71..f938a21af35f 100644 --- a/packages/compiler/src/render3/r3_identifiers.ts +++ b/packages/compiler/src/render3/r3_identifiers.ts @@ -320,4 +320,5 @@ export class Identifiers { static sanitizeUrl: o.ExternalReference = {name: 'ɵɵsanitizeUrl', moduleName: CORE}; static sanitizeUrlOrResourceUrl: o.ExternalReference = {name: 'ɵɵsanitizeUrlOrResourceUrl', moduleName: CORE}; + static trustHtml: o.ExternalReference = {name: 'ɵɵtrustHtml', moduleName: CORE}; } diff --git a/packages/compiler/src/render3/view/template.ts b/packages/compiler/src/render3/view/template.ts index eb92a1590c4d..2780ab180652 100644 --- a/packages/compiler/src/render3/view/template.ts +++ b/packages/compiler/src/render3/view/template.ts @@ -1296,7 +1296,15 @@ export class TemplateDefinitionBuilder implements t.Visitor, LocalResolver if (attr.name === NG_PROJECT_AS_ATTR_NAME) { ngProjectAsAttr = attr; } - attrExprs.push(...getAttributeNameLiterals(attr.name), asLiteral(attr.value)); + + let value = asLiteral(attr.value); + // TODO: Use the security contract module + if (attr.name === 'srcdoc') { + value = o.taggedTemplate( + o.importExpr(R3.trustHtml), + new o.TemplateLiteral([new o.TemplateLiteralElement(attr.value, attr.valueSpan)], [])); + } + attrExprs.push(...getAttributeNameLiterals(attr.name), value); }); // Keep ngProjectAs next to the other name, value pairs so we can verify that we match diff --git a/packages/core/src/core_render3_private_export.ts b/packages/core/src/core_render3_private_export.ts index 91a0d77b0e81..5f620d3aec0f 100644 --- a/packages/core/src/core_render3_private_export.ts +++ b/packages/core/src/core_render3_private_export.ts @@ -290,6 +290,7 @@ export { ɵɵsanitizeStyle, ɵɵsanitizeUrl, ɵɵsanitizeUrlOrResourceUrl, + ɵɵtrustHtml, } from './sanitization/sanitization'; export { noSideEffects as ɵnoSideEffects, diff --git a/packages/core/src/reflection/reflection_capabilities.ts b/packages/core/src/reflection/reflection_capabilities.ts index 75575ebb135f..c2e28dea040f 100644 --- a/packages/core/src/reflection/reflection_capabilities.ts +++ b/packages/core/src/reflection/reflection_capabilities.ts @@ -74,9 +74,11 @@ export function isDelegateCtor(typeStr: string): boolean { export class ReflectionCapabilities implements PlatformReflectionCapabilities { private _reflect: any; + private _trustedFunction?: (...args: string[]) => Function; - constructor(reflect?: any) { + constructor(reflect?: any, trustedFunction?: (...args: string[]) => Function) { this._reflect = reflect || global['Reflect']; + this._trustedFunction = trustedFunction; } isReflectionEnabled(): boolean { @@ -279,17 +281,25 @@ export class ReflectionCapabilities implements PlatformReflectionCapabilities { } getter(name: string): GetterFn { - return new Function('o', 'return o.' + name + ';'); + const functionBody = 'return o.' + name + ';'; + return ( + this._trustedFunction ? this._trustedFunction('o', functionBody) : + new Function('o', functionBody)); } setter(name: string): SetterFn { - return new Function('o', 'v', 'return o.' + name + ' = v;'); + const functionBody = 'return o.' + name + ' = v;'; + return ( + this._trustedFunction ? this._trustedFunction('o', 'v', functionBody) : + new Function('o', 'v', functionBody)); } method(name: string): MethodFn { const functionBody = `if (!o.${name}) throw new Error('"${name}" is undefined'); return o.${name}.apply(o, args);`; - return new Function('o', 'args', functionBody); + return ( + this._trustedFunction ? this._trustedFunction('o', 'args', functionBody) : + new Function('o', 'args', functionBody)); } // There is not a concept of import uri in Js, but this is useful in developing Dart applications. diff --git a/packages/core/src/render3/i18n/i18n_parse.ts b/packages/core/src/render3/i18n/i18n_parse.ts index 37949652438c..5821303bf218 100644 --- a/packages/core/src/render3/i18n/i18n_parse.ts +++ b/packages/core/src/render3/i18n/i18n_parse.ts @@ -10,6 +10,7 @@ import '../../util/ng_i18n_closure_mode'; import {getTemplateContent, SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS} from '../../sanitization/html_sanitizer'; import {getInertBodyHelper} from '../../sanitization/inert_body'; +import {getTrustedTypesPolicy} from '../../sanitization/trusted_types'; import {_sanitizeUrl, sanitizeSrcset} from '../../sanitization/url_sanitizer'; import {addAllToArray} from '../../util/array_utils'; import {assertEqual} from '../../util/assert'; @@ -524,7 +525,7 @@ export function parseICUBlock(pattern: string): IcuExpression { function parseIcuCase( unsafeHtml: string, parentIndex: number, nestedIcus: IcuExpression[], tIcus: TIcu[], expandoStartIndex: number): IcuCase { - const inertBodyHelper = getInertBodyHelper(getDocument()); + const inertBodyHelper = getInertBodyHelper(getDocument(), getTrustedTypesPolicy()); const inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml); if (!inertBodyElement) { throw new Error('Unable to generate inert body element'); diff --git a/packages/core/src/render3/interfaces/renderer.ts b/packages/core/src/render3/interfaces/renderer.ts index f2cd45492eed..69e8c9c67c83 100644 --- a/packages/core/src/render3/interfaces/renderer.ts +++ b/packages/core/src/render3/interfaces/renderer.ts @@ -80,7 +80,9 @@ export interface ProceduralRenderer3 { parentNode(node: RNode): RElement|null; nextSibling(node: RNode): RNode|null; - setAttribute(el: RElement, name: string, value: string, namespace?: string|null): void; + setAttribute( + el: RElement, name: string, value: string|TrustedHTML|TrustedScript|TrustedScriptURL, + namespace?: string|null): void; removeAttribute(el: RElement, name: string, namespace?: string|null): void; addClass(el: RElement, name: string): void; removeClass(el: RElement, name: string): void; @@ -88,7 +90,8 @@ export interface ProceduralRenderer3 { el: RElement, style: string, value: any, flags?: RendererStyleFlags2|RendererStyleFlags3): void; removeStyle(el: RElement, style: string, flags?: RendererStyleFlags2|RendererStyleFlags3): void; - setProperty(el: RElement, name: string, value: any): void; + setProperty(el: RElement, name: string|TrustedHTML|TrustedScript|TrustedScriptURL, value: any): + void; setValue(node: RText|RComment, value: string): void; // TODO(misko): Deprecate in favor of addEventListener/removeEventListener @@ -157,9 +160,11 @@ export interface RElement extends RNode { classList: RDomTokenList; className: string; textContent: string|null; - setAttribute(name: string, value: string): void; + setAttribute(name: string, value: string|TrustedHTML|TrustedScript|TrustedScriptURL): void; removeAttribute(name: string): void; - setAttributeNS(namespaceURI: string, qualifiedName: string, value: string): void; + setAttributeNS( + namespaceURI: string, qualifiedName: string, + value: string|TrustedHTML|TrustedScript|TrustedScriptURL): void; addEventListener(type: string, listener: EventListener, useCapture?: boolean): void; removeEventListener(type: string, listener?: EventListener, options?: boolean): void; diff --git a/packages/core/src/render3/interfaces/sanitization.ts b/packages/core/src/render3/interfaces/sanitization.ts index 886ff809c956..d911e9b0cebc 100644 --- a/packages/core/src/render3/interfaces/sanitization.ts +++ b/packages/core/src/render3/interfaces/sanitization.ts @@ -9,4 +9,5 @@ /** * Function used to sanitize the value before writing it into the renderer. */ -export type SanitizerFn = (value: any, tagName?: string, propName?: string) => string; +export type SanitizerFn = (value: any, tagName?: string, propName?: string) => + string|TrustedHTML|TrustedScript|TrustedScriptURL; diff --git a/packages/core/src/render3/jit/environment.ts b/packages/core/src/render3/jit/environment.ts index 894fe7711a55..e6a0f58a1ce1 100644 --- a/packages/core/src/render3/jit/environment.ts +++ b/packages/core/src/render3/jit/environment.ts @@ -166,4 +166,5 @@ export const angularCoreEnv: {[name: string]: Function} = 'ɵɵsanitizeScript': sanitization.ɵɵsanitizeScript, 'ɵɵsanitizeUrl': sanitization.ɵɵsanitizeUrl, 'ɵɵsanitizeUrlOrResourceUrl': sanitization.ɵɵsanitizeUrlOrResourceUrl, + 'ɵɵtrustHtml': sanitization.ɵɵtrustHtml, }))(); diff --git a/packages/core/src/sanitization/bypass.ts b/packages/core/src/sanitization/bypass.ts index 0d16a1098487..39d6d946936f 100644 --- a/packages/core/src/sanitization/bypass.ts +++ b/packages/core/src/sanitization/bypass.ts @@ -58,8 +58,8 @@ export interface SafeUrl extends SafeValue {} export interface SafeResourceUrl extends SafeValue {} -abstract class SafeValueImpl implements SafeValue { - constructor(public changingThisBreaksApplicationSecurity: string) {} +abstract class SafeValueImpl implements SafeValue { + constructor(public changingThisBreaksApplicationSecurity: string|T) {} abstract getTypeName(): string; @@ -69,35 +69,35 @@ abstract class SafeValueImpl implements SafeValue { } } -class SafeHtmlImpl extends SafeValueImpl implements SafeHtml { +class SafeHtmlImpl extends SafeValueImpl implements SafeHtml { getTypeName() { return BypassType.Html; } } -class SafeStyleImpl extends SafeValueImpl implements SafeStyle { +class SafeStyleImpl extends SafeValueImpl implements SafeStyle { getTypeName() { return BypassType.Style; } } -class SafeScriptImpl extends SafeValueImpl implements SafeScript { +class SafeScriptImpl extends SafeValueImpl implements SafeScript { getTypeName() { return BypassType.Script; } } -class SafeUrlImpl extends SafeValueImpl implements SafeUrl { +class SafeUrlImpl extends SafeValueImpl implements SafeUrl { getTypeName() { return BypassType.Url; } } -class SafeResourceUrlImpl extends SafeValueImpl implements SafeResourceUrl { +class SafeResourceUrlImpl extends SafeValueImpl implements SafeResourceUrl { getTypeName() { return BypassType.ResourceUrl; } } -export function unwrapSafeValue(value: SafeValue): string; +export function unwrapSafeValue(value: SafeValue): string|T; export function unwrapSafeValue(value: T): T; -export function unwrapSafeValue(value: T|SafeValue): T { +export function unwrapSafeValue(value: any): string|T { return value instanceof SafeValueImpl ? value.changingThisBreaksApplicationSecurity as any as T : value as any as T; } @@ -137,7 +137,7 @@ export function getSanitizationBypassType(value: any): BypassType|null { * @param trustedHtml `html` string which needs to be implicitly trusted. * @returns a `html` which has been branded to be implicitly trusted. */ -export function bypassSanitizationTrustHtml(trustedHtml: string): SafeHtml { +export function bypassSanitizationTrustHtml(trustedHtml: string|TrustedHTML): SafeHtml { return new SafeHtmlImpl(trustedHtml); } /** @@ -161,7 +161,7 @@ export function bypassSanitizationTrustStyle(trustedStyle: string): SafeStyle { * @param trustedScript `script` string which needs to be implicitly trusted. * @returns a `script` which has been branded to be implicitly trusted. */ -export function bypassSanitizationTrustScript(trustedScript: string): SafeScript { +export function bypassSanitizationTrustScript(trustedScript: string|TrustedScript): SafeScript { return new SafeScriptImpl(trustedScript); } /** @@ -185,6 +185,7 @@ export function bypassSanitizationTrustUrl(trustedUrl: string): SafeUrl { * @param trustedResourceUrl `url` string which needs to be implicitly trusted. * @returns a `url` which has been branded to be implicitly trusted. */ -export function bypassSanitizationTrustResourceUrl(trustedResourceUrl: string): SafeResourceUrl { +export function bypassSanitizationTrustResourceUrl(trustedResourceUrl: string| + TrustedScriptURL): SafeResourceUrl { return new SafeResourceUrlImpl(trustedResourceUrl); } diff --git a/packages/core/src/sanitization/html_sanitizer.ts b/packages/core/src/sanitization/html_sanitizer.ts index ab7bbbe8369d..49a535e8bba5 100644 --- a/packages/core/src/sanitization/html_sanitizer.ts +++ b/packages/core/src/sanitization/html_sanitizer.ts @@ -8,6 +8,7 @@ import {isDevMode} from '../util/is_dev_mode'; import {getInertBodyHelper, InertBodyHelper} from './inert_body'; +import {getTrustedTypesPolicy} from './trusted_types'; import {_sanitizeUrl, sanitizeSrcset} from './url_sanitizer'; function tagSet(tags: string): {[k: string]: boolean} { @@ -242,10 +243,10 @@ let inertBodyHelper: InertBodyHelper; * Sanitizes the given unsafe, untrusted HTML fragment, and returns HTML text that is safe to add to * the DOM in a browser environment. */ -export function _sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string { +export function _sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string|TrustedHTML { let inertBodyElement: HTMLElement|null = null; try { - inertBodyHelper = inertBodyHelper || getInertBodyHelper(defaultDoc); + inertBodyHelper = inertBodyHelper || getInertBodyHelper(defaultDoc, getTrustedTypesPolicy()); // Make sure unsafeHtml is actually a string (TypeScript types are not enforced at runtime). let unsafeHtml = unsafeHtmlInput ? String(unsafeHtmlInput) : ''; inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml); @@ -274,7 +275,7 @@ export function _sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string 'WARNING: sanitizing HTML stripped some content, see http://g.co/ng/security#xss'); } - return safeHtml; + return getTrustedTypesPolicy()?.createHTML(safeHtml) ?? safeHtml; } finally { // In case anything goes wrong, clear out inertElement to reset the entire DOM structure. if (inertBodyElement) { diff --git a/packages/core/src/sanitization/inert_body.ts b/packages/core/src/sanitization/inert_body.ts index 0d7173f01a52..28ed38153e59 100644 --- a/packages/core/src/sanitization/inert_body.ts +++ b/packages/core/src/sanitization/inert_body.ts @@ -13,8 +13,10 @@ * Default: DOMParser strategy * Fallback: InertDocument strategy */ -export function getInertBodyHelper(defaultDoc: Document): InertBodyHelper { - return isDOMParserAvailable() ? new DOMParserHelper() : new InertDocumentHelper(defaultDoc); +export function getInertBodyHelper( + defaultDoc: Document, trustedTypePolicy?: TrustedTypePolicy|null): InertBodyHelper { + return isDOMParserAvailable() ? new DOMParserHelper(trustedTypePolicy) : + new InertDocumentHelper(defaultDoc, trustedTypePolicy); } export interface InertBodyHelper { @@ -29,6 +31,8 @@ export interface InertBodyHelper { * This is the default strategy used in browsers that support it. */ class DOMParserHelper implements InertBodyHelper { + constructor(private readonly trustedTypePolicy?: TrustedTypePolicy|null) {} + getInertBodyElement(html: string): HTMLElement|null { // We add these extra elements to ensure that the rest of the content is parsed as expected // e.g. leading whitespace is maintained and tags like `` do not get hoisted to the @@ -36,8 +40,11 @@ class DOMParserHelper implements InertBodyHelper { // in `html` from consuming the otherwise explicit `` tag. html = '' + html; try { - const body = new (window as any).DOMParser().parseFromString(html, 'text/html').body as - HTMLBodyElement; + const body = + new (window as any) + .DOMParser() + .parseFromString(this.trustedTypePolicy?.createHTML(html) ?? html, 'text/html') + .body as HTMLBodyElement; body.removeChild(body.firstChild!); return body; } catch { @@ -54,7 +61,8 @@ class DOMParserHelper implements InertBodyHelper { class InertDocumentHelper implements InertBodyHelper { private inertDocument: Document; - constructor(private defaultDoc: Document) { + constructor( + private defaultDoc: Document, private readonly trustedTypePolicy?: TrustedTypePolicy|null) { this.inertDocument = this.defaultDoc.implementation.createHTMLDocument('sanitization-inert'); if (this.inertDocument.body == null) { @@ -71,7 +79,8 @@ class InertDocumentHelper implements InertBodyHelper { // Prefer using