Skip to content

Commit 03ec620

Browse files
bjarkleralxhub
authored andcommitted
fix(upgrade): Address Trusted Types violations in @angular/upgrade (#57454)
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. PR Close #57454
1 parent 6d3a2af commit 03ec620

File tree

5 files changed

+123
-19
lines changed

5 files changed

+123
-19
lines changed

adev/src/content/guide/security.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -201,12 +201,13 @@ See [caniuse.com/trusted-types](https://caniuse.com/trusted-types) for the curre
201201

202202
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:
203203

204-
| Policies | Detail |
205-
| :---------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
206-
| `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. |
207-
| `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. |
208-
| `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). |
209-
| `angular#bundler` | This policy is used by the Angular CLI bundler when creating lazy chunk files. |
204+
| Policies | Detail |
205+
| :----------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
206+
| `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. |
207+
| `angular#bundler` | This policy is used by the Angular CLI bundler when creating lazy chunk files. |
208+
| `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. |
209+
| `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). |
210+
| `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. |
210211

211212
You should configure the HTTP headers for Trusted Types in the following locations:
212213

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
/**
10+
* @fileoverview
11+
* A module to facilitate use of a Trusted Types policy internally within
12+
* the upgrade package. It lazily constructs the Trusted Types policy, providing
13+
* helper utilities for promoting strings to Trusted Types. When Trusted Types
14+
* are not available, strings are used as a fallback.
15+
* @security All use of this module is security-sensitive and should go through
16+
* security review.
17+
*/
18+
19+
import {TrustedHTML, TrustedTypePolicy, TrustedTypePolicyFactory} from './trusted_types_defs';
20+
21+
/**
22+
* The Trusted Types policy, or null if Trusted Types are not
23+
* enabled/supported, or undefined if the policy has not been created yet.
24+
*/
25+
let policy: TrustedTypePolicy | null | undefined;
26+
27+
/**
28+
* Returns the Trusted Types policy, or null if Trusted Types are not
29+
* enabled/supported. The first call to this function will create the policy.
30+
*/
31+
function getPolicy(): TrustedTypePolicy | null {
32+
if (policy === undefined) {
33+
policy = null;
34+
const windowWithTrustedTypes = window as unknown as {trustedTypes?: TrustedTypePolicyFactory};
35+
if (windowWithTrustedTypes.trustedTypes) {
36+
try {
37+
policy = windowWithTrustedTypes.trustedTypes.createPolicy('angular#unsafe-upgrade', {
38+
createHTML: (s: string) => s,
39+
});
40+
} catch {
41+
// trustedTypes.createPolicy throws if called with a name that is
42+
// already registered, even in report-only mode. Until the API changes,
43+
// catch the error not to break the applications functionally. In such
44+
// cases, the code will fall back to using strings.
45+
}
46+
}
47+
}
48+
return policy;
49+
}
50+
51+
/**
52+
* Unsafely promote a legacy AngularJS template to a TrustedHTML, falling back
53+
* to strings when Trusted Types are not available.
54+
* @security This is a security-sensitive function; any use of this function
55+
* must go through security review. In particular, the template string should
56+
* always be under full control of the application author, as untrusted input
57+
* can cause an XSS vulnerability.
58+
*/
59+
export function trustedHTMLFromLegacyTemplate(html: string): TrustedHTML | string {
60+
return getPolicy()?.createHTML(html) || html;
61+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
/**
10+
* @fileoverview
11+
* While Angular only uses Trusted Types internally for the time being,
12+
* references to Trusted Types could leak into our public API, which would force
13+
* anyone compiling against @angular/upgrade to provide the @types/trusted-types
14+
* package in their compilation unit.
15+
*
16+
* Until https://github.com/microsoft/TypeScript/issues/30024 is resolved, we
17+
* will keep Angular's public API surface free of references to Trusted Types.
18+
* For internal and semi-private APIs that need to reference Trusted Types, the
19+
* minimal type definitions for the Trusted Types API provided by this module
20+
* should be used instead. They are marked as "declare" to prevent them from
21+
* being renamed by compiler optimization.
22+
*
23+
* Adapted from
24+
* https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/trusted-types/index.d.ts
25+
* but restricted to the API surface used within Angular, mimicking the approach
26+
* in packages/core/src/util/security/trusted_type_defs.ts.
27+
*/
28+
29+
export type TrustedHTML = string & {
30+
__brand__: 'TrustedHTML';
31+
};
32+
33+
export interface TrustedTypePolicyFactory {
34+
createPolicy(
35+
policyName: string,
36+
policyOptions: {createHTML?: (input: string) => string},
37+
): TrustedTypePolicy;
38+
}
39+
40+
export interface TrustedTypePolicy {
41+
createHTML(input: string): TrustedHTML;
42+
}

packages/upgrade/src/common/src/upgrade_helper.ts

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import {
2626
} from './angular1';
2727
import {$COMPILE, $CONTROLLER, $HTTP_BACKEND, $INJECTOR, $TEMPLATE_CACHE} from './constants';
2828
import {cleanData, controllerKey, directiveNormalize, isFunction} from './util';
29+
import {TrustedHTML} from './security/trusted_types_defs';
30+
import {trustedHTMLFromLegacyTemplate} from './security/trusted_types';
2931

3032
// Constants
3133
const REQUIRE_PREFIX_RE = /^(\^\^?)?(\?)?(\^\^?)?/;
@@ -91,16 +93,16 @@ export class UpgradeHelper {
9193
directive: IDirective,
9294
fetchRemoteTemplate = false,
9395
$element?: IAugmentedJQuery,
94-
): string | Promise<string> {
96+
): string | TrustedHTML | Promise<string | TrustedHTML> {
9597
if (directive.template !== undefined) {
96-
return getOrCall<string>(directive.template, $element);
98+
return trustedHTMLFromLegacyTemplate(getOrCall<string>(directive.template, $element));
9799
} else if (directive.templateUrl) {
98100
const $templateCache = $injector.get($TEMPLATE_CACHE) as ITemplateCacheService;
99101
const url = getOrCall<string>(directive.templateUrl, $element);
100102
const template = $templateCache.get(url);
101103

102104
if (template !== undefined) {
103-
return template;
105+
return trustedHTMLFromLegacyTemplate(template);
104106
} else if (!fetchRemoteTemplate) {
105107
throw new Error('loading directive templates asynchronously is not supported');
106108
}
@@ -109,7 +111,7 @@ export class UpgradeHelper {
109111
const $httpBackend = $injector.get($HTTP_BACKEND) as IHttpBackendService;
110112
$httpBackend('GET', url, null, (status: number, response: string) => {
111113
if (status === 200) {
112-
resolve($templateCache.put(url, response));
114+
resolve(trustedHTMLFromLegacyTemplate($templateCache.put(url, response)));
113115
} else {
114116
reject(`GET component template from '${url}' returned '${status}: ${response}'`);
115117
}
@@ -131,14 +133,11 @@ export class UpgradeHelper {
131133
return controller;
132134
}
133135

134-
compileTemplate(template?: string): ILinkFn {
136+
compileTemplate(template?: string | TrustedHTML): ILinkFn {
135137
if (template === undefined) {
136-
template = UpgradeHelper.getTemplate(
137-
this.$injector,
138-
this.directive,
139-
false,
140-
this.$element,
141-
) as string;
138+
template = UpgradeHelper.getTemplate(this.$injector, this.directive, false, this.$element) as
139+
| string
140+
| TrustedHTML;
142141
}
143142

144143
return this.compileHtml(template);
@@ -251,7 +250,7 @@ export class UpgradeHelper {
251250
return requiredControllers;
252251
}
253252

254-
private compileHtml(html: string): ILinkFn {
253+
private compileHtml(html: string | TrustedHTML): ILinkFn {
255254
this.element.innerHTML = html;
256255
return this.$compile(this.element.childNodes);
257256
}

packages/upgrade/src/dynamic/src/upgrade_ng1_adapter.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import {
3636
UpgradeHelper,
3737
} from '../../common/src/upgrade_helper';
3838
import {isFunction, strictEquals} from '../../common/src/util';
39+
import {trustedHTMLFromLegacyTemplate} from '../../common/src/security/trusted_types';
3940

4041
const CAMEL_CASE = /([A-Z])/g;
4142
const INITIAL_VALUE = {
@@ -230,7 +231,7 @@ class UpgradeNg1ComponentAdapter implements OnInit, OnChanges, DoCheck {
230231
ngOnInit() {
231232
// Collect contents, insert and compile template
232233
const attachChildNodes: ILinkFn | undefined = this.helper.prepareTransclusion();
233-
const linkFn = this.helper.compileTemplate(this.template);
234+
const linkFn = this.helper.compileTemplate(trustedHTMLFromLegacyTemplate(this.template));
234235

235236
// Instantiate controller (if not already done so)
236237
const controllerType = this.directive.controller;

0 commit comments

Comments
 (0)