diff --git a/src/material/icon/icon-registry.ts b/src/material/icon/icon-registry.ts index 164704a04224..52f6cec074cc 100644 --- a/src/material/icon/icon-registry.ts +++ b/src/material/icon/icon-registry.ts @@ -21,6 +21,7 @@ import { import {DomSanitizer, SafeResourceUrl, SafeHtml} from '@angular/platform-browser'; import {forkJoin, Observable, of as observableOf, throwError as observableThrow} from 'rxjs'; import {catchError, finalize, map, share, tap} from 'rxjs/operators'; +import {TrustedHTML, trustedHTMLFromString} from './trusted-types'; /** @@ -96,12 +97,12 @@ class SvgIconConfig { constructor( public url: SafeResourceUrl, - public svgText: string | null, + public svgText: TrustedHTML | null, public options?: IconOptions) {} } /** Icon configuration whose content has already been loaded. */ -type LoadedSvgIconConfig = SvgIconConfig & {svgText: string}; +type LoadedSvgIconConfig = SvgIconConfig & {svgText: TrustedHTML}; /** * Service to register and display icons used by the `` component. @@ -129,7 +130,7 @@ export class MatIconRegistry implements OnDestroy { private _cachedIconsByUrl = new Map(); /** In-progress icon fetches. Used to coalesce multiple requests to the same URL. */ - private _inProgressUrlFetches = new Map>(); + private _inProgressUrlFetches = new Map>(); /** Map from font identifiers to their CSS class names. Used for icon fonts. */ private _fontCssClassesByAlias = new Map(); @@ -209,8 +210,10 @@ export class MatIconRegistry implements OnDestroy { throw getMatIconFailedToSanitizeLiteralError(literal); } + // Security: The literal is passed in as SafeHtml, and is thus trusted. + const trustedLiteral = trustedHTMLFromString(cleanLiteral); return this._addSvgIconConfig(namespace, iconName, - new SvgIconConfig('', cleanLiteral, options)); + new SvgIconConfig('', trustedLiteral, options)); } /** @@ -251,7 +254,9 @@ export class MatIconRegistry implements OnDestroy { throw getMatIconFailedToSanitizeLiteralError(literal); } - return this._addSvgIconSetConfig(namespace, new SvgIconConfig('', cleanLiteral, options)); + // Security: The literal is passed in as SafeHtml, and is thus trusted. + const trustedLiteral = trustedHTMLFromString(cleanLiteral); + return this._addSvgIconSetConfig(namespace, new SvgIconConfig('', trustedLiteral, options)); } /** @@ -399,7 +404,7 @@ export class MatIconRegistry implements OnDestroy { // Not found in any cached icon sets. If there are icon sets with URLs that we haven't // fetched, fetch them now and look for iconName in the results. - const iconSetFetchRequests: Observable[] = iconSetConfigs + const iconSetFetchRequests: Observable[] = iconSetConfigs .filter(iconSetConfig => !iconSetConfig.svgText) .map(iconSetConfig => { return this._loadSvgIconSetFromConfig(iconSetConfig).pipe( @@ -444,7 +449,7 @@ export class MatIconRegistry implements OnDestroy { // the parsing by doing a quick check using `indexOf` to see if there's any chance for the // icon to be in the set. This won't be 100% accurate, but it should help us avoid at least // some of the parsing. - if (config.svgText && config.svgText.indexOf(iconName) > -1) { + if (config.svgText && config.svgText.toString().indexOf(iconName) > -1) { const svg = this._svgElementFromConfig(config as LoadedSvgIconConfig); const foundIcon = this._extractSvgIconFromSet(svg, iconName, config.options); if (foundIcon) { @@ -470,7 +475,7 @@ export class MatIconRegistry implements OnDestroy { * Loads the content of the icon set URL specified in the * SvgIconConfig and attaches it to the config. */ - private _loadSvgIconSetFromConfig(config: SvgIconConfig): Observable { + private _loadSvgIconSetFromConfig(config: SvgIconConfig): Observable { if (config.svgText) { return observableOf(null); } @@ -516,7 +521,7 @@ export class MatIconRegistry implements OnDestroy { // have to create an empty SVG node using innerHTML and append its content. // Elements created using DOMParser.parseFromString have the same problem. // http://stackoverflow.com/questions/23003278/svg-innerhtml-in-firefox-can-not-display - const svg = this._svgElementFromString(''); + const svg = this._svgElementFromString(trustedHTMLFromString('')); // Clone the node so we don't remove it from the parent icon set element. svg.appendChild(iconElement); @@ -526,9 +531,9 @@ export class MatIconRegistry implements OnDestroy { /** * Creates a DOM element from the given SVG string. */ - private _svgElementFromString(str: string): SVGElement { + private _svgElementFromString(str: TrustedHTML): SVGElement { const div = this._document.createElement('DIV'); - div.innerHTML = str; + div.innerHTML = str as unknown as string; const svg = div.querySelector('svg') as SVGElement; // TODO: add an ngDevMode check @@ -543,7 +548,7 @@ export class MatIconRegistry implements OnDestroy { * Converts an element into an SVG node by cloning all of its children. */ private _toSvgElement(element: Element): SVGElement { - const svg = this._svgElementFromString(''); + const svg = this._svgElementFromString(trustedHTMLFromString('')); const attributes = element.attributes; // Copy over all the attributes from the `symbol` to the new SVG, except the id. @@ -585,7 +590,7 @@ export class MatIconRegistry implements OnDestroy { * Returns an Observable which produces the string contents of the given icon. Results may be * cached, so future calls with the same URL may not cause another HTTP request. */ - private _fetchIcon(iconConfig: SvgIconConfig): Observable { + private _fetchIcon(iconConfig: SvgIconConfig): Observable { const {url: safeUrl, options} = iconConfig; const withCredentials = options?.withCredentials ?? false; @@ -615,6 +620,11 @@ export class MatIconRegistry implements OnDestroy { } const req = this._httpClient.get(url, {responseType: 'text', withCredentials}).pipe( + map(svg => { + // Security: This SVG is fetched from a SafeResourceUrl, and is thus + // trusted HTML. + return trustedHTMLFromString(svg); + }), finalize(() => this._inProgressUrlFetches.delete(url)), share(), ); diff --git a/src/material/icon/trusted-types.ts b/src/material/icon/trusted-types.ts new file mode 100644 index 000000000000..78d12b5526ec --- /dev/null +++ b/src/material/icon/trusted-types.ts @@ -0,0 +1,69 @@ +/** + * @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 + * Angular Material. 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. + */ + +export declare interface TrustedHTML { + __brand__: 'TrustedHTML'; +} + +export declare interface TrustedTypePolicyFactory { + createPolicy(policyName: string, policyOptions: { + createHTML?: (input: string) => string, + }): TrustedTypePolicy; +} + +export declare interface TrustedTypePolicy { + createHTML(input: string): TrustedHTML; +} + +/** + * 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; + +/** + * 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 (typeof window !== 'undefined') { + const ttWindow = window as unknown as {trustedTypes?: TrustedTypePolicyFactory}; + if (ttWindow.trustedTypes !== undefined) { + policy = ttWindow.trustedTypes.createPolicy('angular#components', { + createHTML: (s: string) => s, + }); + } + } + } + return policy; +} + +/** + * Unsafely promote a string 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, it must be assured that the + * provided string will never cause an XSS vulnerability if used in a context + * that will be interpreted as HTML by a browser, e.g. when assigning to + * element.innerHTML. + */ +export function trustedHTMLFromString(html: string): TrustedHTML { + return getPolicy()?.createHTML(html) || html as unknown as TrustedHTML; +}