Skip to content

Commit 881edec

Browse files
authored
fix(material/icon): make icon-registry compatible with Trusted Types (#23140)
When Angular Material is used in an environment that enforces Trusted Types, the icon registry raises a Trusted Types violation due to its use of element.innerHTML when initializing SVG icons. To make the icon registry compatible with Trusted Types, SvgIconConfig.svgText is changed to a TrustedHTML, and its users updated to either produce TrustedHTML (making sure to only do so in cases where its security can be readily assessed) or pass such values along. To facilitate this, add a module that provides a Trusted Types policy, 'angular#components'. The policy is created lazily and stored in a module-local variable. This is the same as the approach taken by Angular proper in https://github.com/angular/angular/blob/master/packages/core/src/util/security/trusted_types.ts
1 parent 2c48fc8 commit 881edec

File tree

2 files changed

+92
-13
lines changed

2 files changed

+92
-13
lines changed

src/material/icon/icon-registry.ts

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121
import {DomSanitizer, SafeResourceUrl, SafeHtml} from '@angular/platform-browser';
2222
import {forkJoin, Observable, of as observableOf, throwError as observableThrow} from 'rxjs';
2323
import {catchError, finalize, map, share, tap} from 'rxjs/operators';
24+
import {TrustedHTML, trustedHTMLFromString} from './trusted-types';
2425

2526

2627
/**
@@ -96,12 +97,12 @@ class SvgIconConfig {
9697

9798
constructor(
9899
public url: SafeResourceUrl,
99-
public svgText: string | null,
100+
public svgText: TrustedHTML | null,
100101
public options?: IconOptions) {}
101102
}
102103

103104
/** Icon configuration whose content has already been loaded. */
104-
type LoadedSvgIconConfig = SvgIconConfig & {svgText: string};
105+
type LoadedSvgIconConfig = SvgIconConfig & {svgText: TrustedHTML};
105106

106107
/**
107108
* Service to register and display icons used by the `<mat-icon>` component.
@@ -129,7 +130,7 @@ export class MatIconRegistry implements OnDestroy {
129130
private _cachedIconsByUrl = new Map<string, SVGElement>();
130131

131132
/** In-progress icon fetches. Used to coalesce multiple requests to the same URL. */
132-
private _inProgressUrlFetches = new Map<string, Observable<string>>();
133+
private _inProgressUrlFetches = new Map<string, Observable<TrustedHTML>>();
133134

134135
/** Map from font identifiers to their CSS class names. Used for icon fonts. */
135136
private _fontCssClassesByAlias = new Map<string, string>();
@@ -209,8 +210,10 @@ export class MatIconRegistry implements OnDestroy {
209210
throw getMatIconFailedToSanitizeLiteralError(literal);
210211
}
211212

213+
// Security: The literal is passed in as SafeHtml, and is thus trusted.
214+
const trustedLiteral = trustedHTMLFromString(cleanLiteral);
212215
return this._addSvgIconConfig(namespace, iconName,
213-
new SvgIconConfig('', cleanLiteral, options));
216+
new SvgIconConfig('', trustedLiteral, options));
214217
}
215218

216219
/**
@@ -251,7 +254,9 @@ export class MatIconRegistry implements OnDestroy {
251254
throw getMatIconFailedToSanitizeLiteralError(literal);
252255
}
253256

254-
return this._addSvgIconSetConfig(namespace, new SvgIconConfig('', cleanLiteral, options));
257+
// Security: The literal is passed in as SafeHtml, and is thus trusted.
258+
const trustedLiteral = trustedHTMLFromString(cleanLiteral);
259+
return this._addSvgIconSetConfig(namespace, new SvgIconConfig('', trustedLiteral, options));
255260
}
256261

257262
/**
@@ -399,7 +404,7 @@ export class MatIconRegistry implements OnDestroy {
399404

400405
// Not found in any cached icon sets. If there are icon sets with URLs that we haven't
401406
// fetched, fetch them now and look for iconName in the results.
402-
const iconSetFetchRequests: Observable<string | null>[] = iconSetConfigs
407+
const iconSetFetchRequests: Observable<TrustedHTML | null>[] = iconSetConfigs
403408
.filter(iconSetConfig => !iconSetConfig.svgText)
404409
.map(iconSetConfig => {
405410
return this._loadSvgIconSetFromConfig(iconSetConfig).pipe(
@@ -444,7 +449,7 @@ export class MatIconRegistry implements OnDestroy {
444449
// the parsing by doing a quick check using `indexOf` to see if there's any chance for the
445450
// icon to be in the set. This won't be 100% accurate, but it should help us avoid at least
446451
// some of the parsing.
447-
if (config.svgText && config.svgText.indexOf(iconName) > -1) {
452+
if (config.svgText && config.svgText.toString().indexOf(iconName) > -1) {
448453
const svg = this._svgElementFromConfig(config as LoadedSvgIconConfig);
449454
const foundIcon = this._extractSvgIconFromSet(svg, iconName, config.options);
450455
if (foundIcon) {
@@ -470,7 +475,7 @@ export class MatIconRegistry implements OnDestroy {
470475
* Loads the content of the icon set URL specified in the
471476
* SvgIconConfig and attaches it to the config.
472477
*/
473-
private _loadSvgIconSetFromConfig(config: SvgIconConfig): Observable<string | null> {
478+
private _loadSvgIconSetFromConfig(config: SvgIconConfig): Observable<TrustedHTML | null> {
474479
if (config.svgText) {
475480
return observableOf(null);
476481
}
@@ -516,7 +521,7 @@ export class MatIconRegistry implements OnDestroy {
516521
// have to create an empty SVG node using innerHTML and append its content.
517522
// Elements created using DOMParser.parseFromString have the same problem.
518523
// http://stackoverflow.com/questions/23003278/svg-innerhtml-in-firefox-can-not-display
519-
const svg = this._svgElementFromString('<svg></svg>');
524+
const svg = this._svgElementFromString(trustedHTMLFromString('<svg></svg>'));
520525
// Clone the node so we don't remove it from the parent icon set element.
521526
svg.appendChild(iconElement);
522527

@@ -526,9 +531,9 @@ export class MatIconRegistry implements OnDestroy {
526531
/**
527532
* Creates a DOM element from the given SVG string.
528533
*/
529-
private _svgElementFromString(str: string): SVGElement {
534+
private _svgElementFromString(str: TrustedHTML): SVGElement {
530535
const div = this._document.createElement('DIV');
531-
div.innerHTML = str;
536+
div.innerHTML = str as unknown as string;
532537
const svg = div.querySelector('svg') as SVGElement;
533538

534539
// TODO: add an ngDevMode check
@@ -543,7 +548,7 @@ export class MatIconRegistry implements OnDestroy {
543548
* Converts an element into an SVG node by cloning all of its children.
544549
*/
545550
private _toSvgElement(element: Element): SVGElement {
546-
const svg = this._svgElementFromString('<svg></svg>');
551+
const svg = this._svgElementFromString(trustedHTMLFromString('<svg></svg>'));
547552
const attributes = element.attributes;
548553

549554
// Copy over all the attributes from the `symbol` to the new SVG, except the id.
@@ -585,7 +590,7 @@ export class MatIconRegistry implements OnDestroy {
585590
* Returns an Observable which produces the string contents of the given icon. Results may be
586591
* cached, so future calls with the same URL may not cause another HTTP request.
587592
*/
588-
private _fetchIcon(iconConfig: SvgIconConfig): Observable<string> {
593+
private _fetchIcon(iconConfig: SvgIconConfig): Observable<TrustedHTML> {
589594
const {url: safeUrl, options} = iconConfig;
590595
const withCredentials = options?.withCredentials ?? false;
591596

@@ -615,6 +620,11 @@ export class MatIconRegistry implements OnDestroy {
615620
}
616621

617622
const req = this._httpClient.get(url, {responseType: 'text', withCredentials}).pipe(
623+
map(svg => {
624+
// Security: This SVG is fetched from a SafeResourceUrl, and is thus
625+
// trusted HTML.
626+
return trustedHTMLFromString(svg);
627+
}),
618628
finalize(() => this._inProgressUrlFetches.delete(url)),
619629
share(),
620630
);

src/material/icon/trusted-types.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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+
* Angular Material. 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+
export declare interface TrustedHTML {
20+
__brand__: 'TrustedHTML';
21+
}
22+
23+
export declare interface TrustedTypePolicyFactory {
24+
createPolicy(policyName: string, policyOptions: {
25+
createHTML?: (input: string) => string,
26+
}): TrustedTypePolicy;
27+
}
28+
29+
export declare interface TrustedTypePolicy {
30+
createHTML(input: string): TrustedHTML;
31+
}
32+
33+
/**
34+
* The Trusted Types policy, or null if Trusted Types are not
35+
* enabled/supported, or undefined if the policy has not been created yet.
36+
*/
37+
let policy: TrustedTypePolicy|null|undefined;
38+
39+
/**
40+
* Returns the Trusted Types policy, or null if Trusted Types are not
41+
* enabled/supported. The first call to this function will create the policy.
42+
*/
43+
function getPolicy(): TrustedTypePolicy|null {
44+
if (policy === undefined) {
45+
policy = null;
46+
if (typeof window !== 'undefined') {
47+
const ttWindow = window as unknown as {trustedTypes?: TrustedTypePolicyFactory};
48+
if (ttWindow.trustedTypes !== undefined) {
49+
policy = ttWindow.trustedTypes.createPolicy('angular#components', {
50+
createHTML: (s: string) => s,
51+
});
52+
}
53+
}
54+
}
55+
return policy;
56+
}
57+
58+
/**
59+
* Unsafely promote a string to a TrustedHTML, falling back to strings when
60+
* Trusted Types are not available.
61+
* @security This is a security-sensitive function; any use of this function
62+
* must go through security review. In particular, it must be assured that the
63+
* provided string will never cause an XSS vulnerability if used in a context
64+
* that will be interpreted as HTML by a browser, e.g. when assigning to
65+
* element.innerHTML.
66+
*/
67+
export function trustedHTMLFromString(html: string): TrustedHTML {
68+
return getPolicy()?.createHTML(html) || html as unknown as TrustedHTML;
69+
}

0 commit comments

Comments
 (0)