Skip to content

Commit 35cfa62

Browse files
committed
fix(material/icon): make icon-registry compatible with Trusted Types
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.
1 parent 8de73de commit 35cfa62

File tree

1 file changed

+23
-13
lines changed

1 file changed

+23
-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 '../core/security/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
);

0 commit comments

Comments
 (0)