Skip to content

Commit

Permalink
fix(upgrade): Address Trusted Types violations in @angular/upgrade
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
bjarkler committed Aug 19, 2024
1 parent 368f36d commit c968dbf
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 20 deletions.
13 changes: 7 additions & 6 deletions adev/src/content/guide/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
68 changes: 68 additions & 0 deletions packages/upgrade/src/common/src/security/trusted_types.ts
Original file line number Diff line number Diff line change
@@ -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;
}
42 changes: 42 additions & 0 deletions packages/upgrade/src/common/src/security/trusted_types_defs.ts
Original file line number Diff line number Diff line change
@@ -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;
}
28 changes: 14 additions & 14 deletions packages/upgrade/src/common/src/upgrade_helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = /^(\^\^?)?(\?)?(\^\^?)?/;
Expand Down Expand Up @@ -91,16 +93,16 @@ export class UpgradeHelper {
directive: IDirective,
fetchRemoteTemplate = false,
$element?: IAugmentedJQuery,
): string | Promise<string> {
): string | TrustedHTML | Promise<string | TrustedHTML> {
if (directive.template !== undefined) {
return getOrCall<string>(directive.template, $element);
return trustedHTMLFromLegacyTemplate(getOrCall<string>(directive.template, $element));
} else if (directive.templateUrl) {
const $templateCache = $injector.get($TEMPLATE_CACHE) as ITemplateCacheService;
const url = getOrCall<string>(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');
}
Expand All @@ -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}'`);
}
Expand All @@ -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);
}
Expand Down Expand Up @@ -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);
}
Expand Down

0 comments on commit c968dbf

Please sign in to comment.