From ead84554b51690265fcfcad782509a60e014d483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bjarki=20=C3=81g=C3=BAst=20Gu=C3=B0mundsson?= Date: Wed, 12 Oct 2022 04:30:07 -0700 Subject: [PATCH] Create safe setter for SVGUseElement.href The attribute contains a URL that points to an SVG fragment to be loaded and presented inside the element. The URL can additionally contain a URL fragment representing the ID of a particular element to fetch from within that fragment. See https://developer.mozilla.org/en-US/docs/Web/SVG/Content_type#iri for details. The element only supports loading same-origin resources, but data: and javascript: URLs could cause XSS (e.g. https://github.com/w3c/trusted-types/issues/357) and are thus sanitized. PiperOrigin-RevId: 480590023 --- src/builders/url_sanitizer.ts | 8 +++++- src/dom/elements/svg_use.ts | 25 ++++++++++++++++++ src/dom/index.ts | 1 + test/dom/elements/svg_use_test.ts | 42 +++++++++++++++++++++++++++++++ 4 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 src/dom/elements/svg_use.ts create mode 100644 test/dom/elements/svg_use_test.ts diff --git a/src/builders/url_sanitizer.ts b/src/builders/url_sanitizer.ts index 1296d06c..4e080bed 100644 --- a/src/builders/url_sanitizer.ts +++ b/src/builders/url_sanitizer.ts @@ -10,7 +10,13 @@ import '../environment/dev'; -function extractScheme(url: string): string|undefined { +/** + * Extracts the scheme from the given URL. If the URL is relative, https: is + * assumed. + * @param url The URL to extract the scheme from. + * @return the URL scheme. + */ +export function extractScheme(url: string): string|undefined { let parsedUrl; try { parsedUrl = new URL(url); diff --git a/src/dom/elements/svg_use.ts b/src/dom/elements/svg_use.ts new file mode 100644 index 00000000..3e3a6fd5 --- /dev/null +++ b/src/dom/elements/svg_use.ts @@ -0,0 +1,25 @@ +/** + * @license + * SPDX-License-Identifier: Apache-2.0 + */ + +import '../../environment/dev'; + +import {extractScheme} from '../../builders/url_sanitizer'; + +/** + * Sets the Href attribute from the given TrustedResourceUrl. + */ +export function setHref(useEl: SVGUseElement, url: string) { + const scheme = extractScheme(url); + if (scheme === 'javascript:' || scheme === 'data:') { + if (process.env.NODE_ENV !== 'production') { + const msg = `A URL with content '${url}' was sanitized away.`; + console.error(msg); + } + return; + } + + // Note that the href property is read-only, so setAttribute must be used. + useEl.setAttribute('href', url); +} diff --git a/src/dom/index.ts b/src/dom/index.ts index 53f9629a..a8611d70 100644 --- a/src/dom/index.ts +++ b/src/dom/index.ts @@ -20,6 +20,7 @@ export * as safeLinkEl from './elements/link'; export * as safeObjectEl from './elements/object'; export * as safeScriptEl from './elements/script'; export * as safeStyleEl from './elements/style'; +export * as safeSvgUseEl from './elements/svg_use'; export * as safeDocument from './globals/document'; export * as safeDomParser from './globals/dom_parser'; export * as safeGlobal from './globals/global'; diff --git a/test/dom/elements/svg_use_test.ts b/test/dom/elements/svg_use_test.ts new file mode 100644 index 00000000..cf581fc7 --- /dev/null +++ b/test/dom/elements/svg_use_test.ts @@ -0,0 +1,42 @@ +/** + * @license + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as svgUseEl from '../../../src/dom/elements/svg_use'; + +describe('svgUseEl', () => { + let element: SVGUseElement; + + beforeEach(() => { + element = document.createElementNS('http://www.w3.org/2000/svg', 'use'); + element.setAttribute('href', 'unchanged'); + }); + + describe('setHref', () => { + it('can set inline resource identifiers', () => { + svgUseEl.setHref(element, '#MyElement'); + expect(element.href.baseVal).toEqual('#MyElement'); + }); + + it('can set relative URLs', () => { + svgUseEl.setHref(element, 'image.svg'); + expect(element.href.baseVal).toEqual('image.svg'); + }); + + it('can set URLs with safe scheme', () => { + svgUseEl.setHref(element, 'https://google.com/image.svg'); + expect(element.href.baseVal).toEqual('https://google.com/image.svg'); + }); + + it('can not set URLs with data: scheme', () => { + svgUseEl.setHref(element, 'data:image/svg+xml,'); + expect(element.href.baseVal).toEqual('unchanged'); + }); + + it('can not set URLs with javascript: scheme', () => { + svgUseEl.setHref(element, 'javascript:alert(1)'); + expect(element.href.baseVal).toEqual('unchanged'); + }); + }); +});