From c968dbf2715cdb097b4250642a8746ceffa50ed4 Mon Sep 17 00:00:00 2001 From: Bjarki Date: Mon, 19 Aug 2024 17:56:05 +0000 Subject: [PATCH] fix(upgrade): Address Trusted Types violations in @angular/upgrade Angular applications that are AngularJS hybrids are currently unable to adopt Trusted Types due to violations eminating from an innerHTML assignment in the @angular/upgrade package. This commit allows developers of such applications to optionally ignore this class of violations by configuring the Trusted Types header to allow the new angular#unsafe-upgrade policy. Note that the policy is explicitly labeled as unsafe as it does not in any way mitigate the security risk of using AngularJS in an Angular application, but does unblock Trusted Types adoption enabling XSS protection for other parts of the application. The implementation follows the approach taken in @angular/core; see packages/core/src/util/security. --- adev/src/content/guide/security.md | 13 ++-- .../src/common/src/security/trusted_types.ts | 68 +++++++++++++++++++ .../common/src/security/trusted_types_defs.ts | 42 ++++++++++++ .../upgrade/src/common/src/upgrade_helper.ts | 28 ++++---- 4 files changed, 131 insertions(+), 20 deletions(-) create mode 100644 packages/upgrade/src/common/src/security/trusted_types.ts create mode 100644 packages/upgrade/src/common/src/security/trusted_types_defs.ts diff --git a/adev/src/content/guide/security.md b/adev/src/content/guide/security.md index 5fe208d0965743..c3b5aaaeae630b 100644 --- a/adev/src/content/guide/security.md +++ b/adev/src/content/guide/security.md @@ -201,12 +201,13 @@ See [caniuse.com/trusted-types](https://caniuse.com/trusted-types) for the curre To enforce Trusted Types for your application, you must configure your application's web server to emit HTTP headers with one of the following Angular policies: -| Policies | Detail | -| :---------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `angular` | This policy is used in security-reviewed code that is internal to Angular, and is required for Angular to function when Trusted Types are enforced. Any inline template values or content sanitized by Angular is treated as safe by this policy. | -| `angular#unsafe-bypass` | This policy is used for applications that use any of the methods in Angular's [DomSanitizer](api/platform-browser/DomSanitizer) that bypass security, such as `bypassSecurityTrustHtml`. Any application that uses these methods must enable this policy. | -| `angular#unsafe-jit` | This policy is used by the [Just-In-Time (JIT) compiler](api/core/Compiler). You must enable this policy if your application interacts directly with the JIT compiler or is running in JIT mode using the [platform browser dynamic](api/platform-browser-dynamic/platformBrowserDynamic). | -| `angular#bundler` | This policy is used by the Angular CLI bundler when creating lazy chunk files. | +| Policies | Detail | +| :----------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `angular` | This policy is used in security-reviewed code that is internal to Angular, and is required for Angular to function when Trusted Types are enforced. Any inline template values or content sanitized by Angular is treated as safe by this policy. | +| `angular#bundler` | This policy is used by the Angular CLI bundler when creating lazy chunk files. | +| `angular#unsafe-bypass` | This policy is used for applications that use any of the methods in Angular's [DomSanitizer](api/platform-browser/DomSanitizer) that bypass security, such as `bypassSecurityTrustHtml`. Any application that uses these methods must enable this policy. | +| `angular#unsafe-jit` | This policy is used by the [Just-In-Time (JIT) compiler](api/core/Compiler). You must enable this policy if your application interacts directly with the JIT compiler or is running in JIT mode using the [platform browser dynamic](api/platform-browser-dynamic/platformBrowserDynamic). | +| `angular#unsafe-upgrade` | This policy is used by the [@angular/upgrade](api/upgrade/static/UpgradeModule) package. You must enable this policy if your application is an AngularJS hybrid. | You should configure the HTTP headers for Trusted Types in the following locations: diff --git a/packages/upgrade/src/common/src/security/trusted_types.ts b/packages/upgrade/src/common/src/security/trusted_types.ts new file mode 100644 index 00000000000000..6a20e6329284ed --- /dev/null +++ b/packages/upgrade/src/common/src/security/trusted_types.ts @@ -0,0 +1,68 @@ +/** + * @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 + */ + +/** + * @fileoverview + * A module to facilitate use of a Trusted Types policy internally within + * the upgrade package. It lazily constructs the Trusted Types policy, providing + * helper utilities for promoting strings to Trusted Types. When Trusted Types + * are not available, strings are used as a fallback. + * @security All use of this module is security-sensitive and should go through + * security review. + */ + +import {TrustedHTML, TrustedTypePolicy, TrustedTypePolicyFactory} from './trusted_types_defs'; + +/** + * The Trusted Types policy, or null if Trusted Types are not + * enabled/supported, or undefined if the policy has not been created yet. + */ +let policy: TrustedTypePolicy | null | undefined; + +/** + * The type of window augmented with the Trusted Types policy factory. + */ +type windowWithTrustedTypes = typeof window & {trustedTypes?: TrustedTypePolicyFactory}; + +/** + * Returns the Trusted Types policy, or null if Trusted Types are not + * enabled/supported. The first call to this function will create the policy. + */ +function getPolicy(): TrustedTypePolicy | null { + if (policy === undefined) { + policy = null; + if ((window as windowWithTrustedTypes).trustedTypes) { + try { + policy = (window as windowWithTrustedTypes).trustedTypes!.createPolicy( + 'angular#unsafe-upgrade', + { + createHTML: (s: string) => s, + }, + ); + } catch { + // 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 + // cases, the code will fall back to using strings. + } + } + } + return policy; +} + +/** + * Unsafely promote a legacy AngularJS template to a TrustedHTML, falling back + * to strings when Trusted Types are not available. + * @security This is a security-sensitive function; any use of this function + * must go through security review. In particular, the template string should + * always be under full control of the application author, as untrusted input + * can cause an XSS vulnerability. + */ +export function trustedHTMLFromLegacyTemplate(html: string): TrustedHTML | string { + return getPolicy()?.createHTML(html) || html; +} diff --git a/packages/upgrade/src/common/src/security/trusted_types_defs.ts b/packages/upgrade/src/common/src/security/trusted_types_defs.ts new file mode 100644 index 00000000000000..7799f7f0ce5026 --- /dev/null +++ b/packages/upgrade/src/common/src/security/trusted_types_defs.ts @@ -0,0 +1,42 @@ +/** + * @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 + */ + +/** + * @fileoverview + * While Angular only uses Trusted Types internally for the time being, + * references to Trusted Types could leak into our public API, which would force + * anyone compiling against @angular/upgrade to provide the @types/trusted-types + * package in their compilation unit. + * + * Until https://github.com/microsoft/TypeScript/issues/30024 is resolved, we + * will keep Angular's public API surface free of references to Trusted Types. + * For internal and semi-private APIs that need to reference Trusted Types, the + * minimal type definitions for the Trusted Types API provided by this module + * should be used instead. They are marked as "declare" to prevent them from + * being renamed by compiler optimization. + * + * Adapted from + * https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/trusted-types/index.d.ts + * but restricted to the API surface used within Angular, mimicking the approach + * in packages/core/src/util/security/trusted_type_defs.ts. + */ + +export type TrustedHTML = string & { + __brand__: 'TrustedHTML'; +}; + +export interface TrustedTypePolicyFactory { + createPolicy( + policyName: string, + policyOptions: {createHTML?: (input: string) => string}, + ): TrustedTypePolicy; +} + +export interface TrustedTypePolicy { + createHTML(input: string): TrustedHTML; +} diff --git a/packages/upgrade/src/common/src/upgrade_helper.ts b/packages/upgrade/src/common/src/upgrade_helper.ts index 2d2bed03e18896..b10db37610d94e 100644 --- a/packages/upgrade/src/common/src/upgrade_helper.ts +++ b/packages/upgrade/src/common/src/upgrade_helper.ts @@ -26,6 +26,8 @@ import { } from './angular1'; import {$COMPILE, $CONTROLLER, $HTTP_BACKEND, $INJECTOR, $TEMPLATE_CACHE} from './constants'; import {cleanData, controllerKey, directiveNormalize, isFunction} from './util'; +import {TrustedHTML} from './security/trusted_types_defs'; +import {trustedHTMLFromLegacyTemplate} from './security/trusted_types'; // Constants const REQUIRE_PREFIX_RE = /^(\^\^?)?(\?)?(\^\^?)?/; @@ -91,16 +93,16 @@ export class UpgradeHelper { directive: IDirective, fetchRemoteTemplate = false, $element?: IAugmentedJQuery, - ): string | Promise { + ): string | TrustedHTML | Promise { if (directive.template !== undefined) { - return getOrCall(directive.template, $element); + return trustedHTMLFromLegacyTemplate(getOrCall(directive.template, $element)); } else if (directive.templateUrl) { const $templateCache = $injector.get($TEMPLATE_CACHE) as ITemplateCacheService; const url = getOrCall(directive.templateUrl, $element); const template = $templateCache.get(url); if (template !== undefined) { - return template; + return trustedHTMLFromLegacyTemplate(template); } else if (!fetchRemoteTemplate) { throw new Error('loading directive templates asynchronously is not supported'); } @@ -109,7 +111,7 @@ export class UpgradeHelper { const $httpBackend = $injector.get($HTTP_BACKEND) as IHttpBackendService; $httpBackend('GET', url, null, (status: number, response: string) => { if (status === 200) { - resolve($templateCache.put(url, response)); + resolve(trustedHTMLFromLegacyTemplate($templateCache.put(url, response))); } else { reject(`GET component template from '${url}' returned '${status}: ${response}'`); } @@ -131,15 +133,13 @@ export class UpgradeHelper { return controller; } - compileTemplate(template?: string): ILinkFn { - if (template === undefined) { - template = UpgradeHelper.getTemplate( - this.$injector, - this.directive, - false, - this.$element, - ) as string; - } + compileTemplate(): ILinkFn { + const template = UpgradeHelper.getTemplate( + this.$injector, + this.directive, + false, + this.$element, + ) as string | TrustedHTML; return this.compileHtml(template); } @@ -251,7 +251,7 @@ export class UpgradeHelper { return requiredControllers; } - private compileHtml(html: string): ILinkFn { + private compileHtml(html: string | TrustedHTML): ILinkFn { this.element.innerHTML = html; return this.$compile(this.element.childNodes); }