From 808f03f4cfcd8188b9f9d86c16ed9b5ef538d5d3 Mon Sep 17 00:00:00 2001 From: Guillaume Weghsteen Date: Mon, 9 Oct 2023 05:49:01 -0700 Subject: [PATCH] Wraps Trusted Types in a Safe Type object implementation. PiperOrigin-RevId: 571913776 --- src/internals/html_impl.ts | 68 +++++++++++++++-------------- src/internals/resource_url_impl.ts | 69 ++++++++++++++++-------------- src/internals/script_impl.ts | 69 ++++++++++++++++-------------- 3 files changed, 109 insertions(+), 97 deletions(-) diff --git a/src/internals/html_impl.ts b/src/internals/html_impl.ts index 92286742..e786c2ea 100644 --- a/src/internals/html_impl.ts +++ b/src/internals/html_impl.ts @@ -5,46 +5,43 @@ import '../environment/dev'; +/* g3_import_pure from './pure' */ import {ensureTokenIsValid, secretToken} from './secrets'; import {getTrustedTypes, getTrustedTypesPolicy} from './trusted_types'; /** - * Runtime implementation of `TrustedHTML` in browsers that don't support it. + * String that is safe to use in HTML contexts in DOM APIs and HTML documents. + * See + * https://github.com/google/safevalues/blob/main/src/README.md#trustedresourceurl */ -class HtmlImpl { - readonly privateDoNotAccessOrElseWrappedHtml: string; +export abstract class SafeHtml { + // tslint:disable-next-line:no-unused-variable + // @ts-ignore + private readonly brand!: never; // To prevent structural typing. +} + +/** Implementation for `SafeHtml` */ +class HtmlImpl extends SafeHtml { + readonly privateDoNotAccessOrElseWrappedHtml: TrustedHTML|string; - constructor(html: string, token: object) { - ensureTokenIsValid(token); + constructor(html: TrustedHTML|string, token: object) { + super(); + if (process.env.NODE_ENV !== 'production') { + ensureTokenIsValid(token); + } this.privateDoNotAccessOrElseWrappedHtml = html; } - toString(): string { + override toString(): string { return this.privateDoNotAccessOrElseWrappedHtml.toString(); } } -function createTrustedHtmlOrPolyfill( - html: string, trusted?: TrustedHTML): SafeHtml { - return (trusted ?? new HtmlImpl(html, secretToken)) as SafeHtml; +function createHtmlInstance(html: string, trusted?: TrustedHTML): SafeHtml { + return new HtmlImpl(trusted ?? html, secretToken); } -const GlobalTrustedHTML = - (typeof window !== undefined) ? window.TrustedHTML : undefined; - -/** - * String that is safe to use in HTML contexts in DOM APIs and HTML - documents. - */ -export type SafeHtml = TrustedHTML; - -/** - * Also exports the constructor so that instanceof checks work. - */ -export const SafeHtml = - (GlobalTrustedHTML ?? HtmlImpl) as unknown as TrustedHTML; - /** * Builds a new `SafeHtml` from the given string, without enforcing safety * guarantees. It may cause side effects by creating a Trusted Types policy. @@ -54,7 +51,7 @@ export const SafeHtml = export function createHtmlInternal(html: string): SafeHtml { /** @noinline */ const noinlineHtml = html; - return createTrustedHtmlOrPolyfill( + return createHtmlInstance( noinlineHtml, getTrustedTypesPolicy()?.createHTML(noinlineHtml)); } @@ -64,13 +61,13 @@ export function createHtmlInternal(html: string): SafeHtml { */ export const EMPTY_HTML: SafeHtml = /* #__PURE__ */ ( - () => createTrustedHtmlOrPolyfill('', getTrustedTypes()?.emptyHTML))(); + () => createHtmlInstance('', getTrustedTypes()?.emptyHTML))(); /** * Checks if the given value is a `SafeHtml` instance. */ export function isHtml(value: unknown): value is SafeHtml { - return getTrustedTypes()?.isHTML(value) || value instanceof HtmlImpl; + return value instanceof HtmlImpl; } /** @@ -78,12 +75,19 @@ export function isHtml(value: unknown): value is SafeHtml { * has the correct type. * * Returns a native `TrustedHTML` or a string if Trusted Types are disabled. + * + * The strange return type is to ensure the value can be used at sinks without a + * cast despite the TypeScript DOM lib not supporting Trusted Types. + * (https://github.com/microsoft/TypeScript/issues/30024) + * + * Note that while the return type is compatible with `string`, you shouldn't + * use any string functions on the result as that will fail in browsers + * supporting Trusted Types. */ -export function unwrapHtml(value: SafeHtml): TrustedHTML|string { - if (getTrustedTypes()?.isHTML(value)) { - return value; - } else if (value instanceof HtmlImpl) { - return value.privateDoNotAccessOrElseWrappedHtml; +export function unwrapHtml(value: SafeHtml): TrustedHTML&string { + if (value instanceof HtmlImpl) { + const unwrapped = value.privateDoNotAccessOrElseWrappedHtml; + return unwrapped as TrustedHTML & string; } else { let message = ''; if (process.env.NODE_ENV !== 'production') { diff --git a/src/internals/resource_url_impl.ts b/src/internals/resource_url_impl.ts index c7df56f5..38d9574b 100644 --- a/src/internals/resource_url_impl.ts +++ b/src/internals/resource_url_impl.ts @@ -6,42 +6,40 @@ import '../environment/dev'; import {ensureTokenIsValid, secretToken} from './secrets'; -import {getTrustedTypes, getTrustedTypesPolicy} from './trusted_types'; +import {getTrustedTypesPolicy} from './trusted_types'; /** - * Runtime implementation of `TrustedScriptURL` in browsers that don't support - * it. + * String that is safe to use in all URL contexts in DOM APIs and HTML + * documents; even as a reference to resources that may load in the current + * origin (e.g. scripts and stylesheets). + + * See + https://github.com/google/safevalues/blob/main/src/README.md#trustedresourceurl */ -class ResourceUrlImpl { - readonly privateDoNotAccessOrElseWrappedResourceUrl: string; +export abstract class TrustedResourceUrl { + // tslint:disable-next-line:no-unused-variable + // @ts-ignore + private readonly brand!: never; // To prevent structural typing. +} - constructor(url: string, token: object) { - ensureTokenIsValid(token); +/** Implementation for `TrustedResourceUrl` */ +class ResourceUrlImpl extends TrustedResourceUrl { + readonly privateDoNotAccessOrElseWrappedResourceUrl: TrustedScriptURL|string; + + constructor(url: TrustedScriptURL|string, token: object) { + super(); + if (process.env.NODE_ENV !== 'production') { + ensureTokenIsValid(token); + } this.privateDoNotAccessOrElseWrappedResourceUrl = url; } - toString(): string { + override toString(): string { return this.privateDoNotAccessOrElseWrappedResourceUrl.toString(); } } -const GlobalTrustedScriptURL = - (typeof window !== undefined) ? window.TrustedScriptURL : undefined; - -/** - * String that is safe to use in all URL contexts in DOM APIs and HTML - * documents; even as a reference to resources that may load in the current - * origin (e.g. scripts and stylesheets). - */ -export type TrustedResourceUrl = TrustedScriptURL; - -/** - * Also exports the constructor so that instanceof checks work. - */ -export const TrustedResourceUrl = - (GlobalTrustedScriptURL ?? ResourceUrlImpl) as unknown as TrustedScriptURL; - /** * Builds a new `TrustedResourceUrl` from the given string, without * enforcing safety guarantees. It may cause side effects by creating a Trusted @@ -53,16 +51,14 @@ export function createResourceUrlInternal(url: string): TrustedResourceUrl { const noinlineUrl = url; const trustedScriptURL = getTrustedTypesPolicy()?.createScriptURL(noinlineUrl); - return (trustedScriptURL ?? new ResourceUrlImpl(noinlineUrl, secretToken)) as - TrustedResourceUrl; + return new ResourceUrlImpl(trustedScriptURL ?? noinlineUrl, secretToken); } /** * Checks if the given value is a `TrustedResourceUrl` instance. */ export function isResourceUrl(value: unknown): value is TrustedResourceUrl { - return getTrustedTypes()?.isScriptURL(value) || - value instanceof ResourceUrlImpl; + return value instanceof ResourceUrlImpl; } /** @@ -71,13 +67,20 @@ export function isResourceUrl(value: unknown): value is TrustedResourceUrl { * * Returns a native `TrustedScriptURL` or a string if Trusted Types are * disabled. + * + * The strange return type is to ensure the value can be used at sinks without a + * cast despite the TypeScript DOM lib not supporting Trusted Types. + * (https://github.com/microsoft/TypeScript/issues/30024) + * + * Note that while the return type is compatible with `string`, you shouldn't + * use any string functions on the result as that will fail in browsers + * supporting Trusted Types. */ -export function unwrapResourceUrl(value: TrustedResourceUrl): TrustedScriptURL| +export function unwrapResourceUrl(value: TrustedResourceUrl): TrustedScriptURL& string { - if (getTrustedTypes()?.isScriptURL(value)) { - return value; - } else if (value instanceof ResourceUrlImpl) { - return value.privateDoNotAccessOrElseWrappedResourceUrl; + if (value instanceof ResourceUrlImpl) { + const unwrapped = value.privateDoNotAccessOrElseWrappedResourceUrl; + return unwrapped as TrustedScriptURL & string; } else { let message = ''; if (process.env.NODE_ENV !== 'production') { diff --git a/src/internals/script_impl.ts b/src/internals/script_impl.ts index b8b87ea6..d0bcbfcc 100644 --- a/src/internals/script_impl.ts +++ b/src/internals/script_impl.ts @@ -5,47 +5,46 @@ import '../environment/dev'; +/* g3_import_pure from './pure' */ import {ensureTokenIsValid, secretToken} from './secrets'; import {getTrustedTypes, getTrustedTypesPolicy} from './trusted_types'; + /** - * Runtime implementation of `TrustedScript` in browswers that don't support it. + * JavaScript code that is safe to evaluate and use as the content of an HTML * script element. + * + * See https://github.com/google/safevalues/blob/main/src/README.md#safescript */ -class ScriptImpl { - readonly privateDoNotAccessOrElseWrappedScript: string; +export abstract class SafeScript { + // tslint:disable-next-line:no-unused-variable + // @ts-ignore + private readonly brand!: never; // To prevent structural typing. +} + +/** Implementation for `SafeScript` */ +class ScriptImpl extends SafeScript { + readonly privateDoNotAccessOrElseWrappedScript: TrustedScript|string; - constructor(script: string, token: object) { - ensureTokenIsValid(token); + constructor(script: TrustedScript|string, token: object) { + super(); + if (process.env.NODE_ENV !== 'production') { + ensureTokenIsValid(token); + } this.privateDoNotAccessOrElseWrappedScript = script; } - toString(): string { + override toString(): string { return this.privateDoNotAccessOrElseWrappedScript.toString(); } } -function createTrustedScriptOrPolyfill( +function createScriptInstance( script: string, trusted?: TrustedScript): SafeScript { - return (trusted ?? new ScriptImpl(script, secretToken)) as SafeScript; + return new ScriptImpl(trusted ?? script, secretToken); } -const GlobalTrustedScript = - (typeof window !== undefined) ? window.TrustedScript : undefined; - -/** - * JavaScript code that is safe to evaluate and use as the content of an HTML - * script element. - */ -export type SafeScript = TrustedScript; - -/** - * Also exports the constructor so that instanceof checks work. - */ -export const SafeScript = - (GlobalTrustedScript ?? ScriptImpl) as unknown as TrustedScript; - /** * Builds a new `SafeScript` from the given string, without enforcing * safety guarantees. It may cause side effects by creating a Trusted Types @@ -55,7 +54,7 @@ export const SafeScript = export function createScriptInternal(script: string): SafeScript { /** @noinline */ const noinlineScript = script; - return createTrustedScriptOrPolyfill( + return createScriptInstance( noinlineScript, getTrustedTypesPolicy()?.createScript(noinlineScript)); } @@ -65,14 +64,13 @@ export function createScriptInternal(script: string): SafeScript { */ export const EMPTY_SCRIPT: SafeScript = /* #__PURE__ */ ( - () => createTrustedScriptOrPolyfill( - '', getTrustedTypes()?.emptyScript))(); + () => createScriptInstance('', getTrustedTypes()?.emptyScript))(); /** * Checks if the given value is a `SafeScript` instance. */ export function isScript(value: unknown): value is SafeScript { - return getTrustedTypes()?.isScript(value) || value instanceof ScriptImpl; + return value instanceof ScriptImpl; } /** @@ -80,12 +78,19 @@ export function isScript(value: unknown): value is SafeScript { * has the correct type. * * Returns a native `TrustedScript` or a string if Trusted Types are disabled. + * + * The strange return type is to ensure the value can be used at sinks without a + * cast despite the TypeScript DOM lib not supporting Trusted Types. + * (https://github.com/microsoft/TypeScript/issues/30024) + * + * Note that while the return type is compatible with `string`, you shouldn't + * use any string functions on the result as that will fail in browsers + * supporting Trusted Types. */ -export function unwrapScript(value: SafeScript): TrustedScript|string { - if (getTrustedTypes()?.isScript(value)) { - return value; - } else if (value instanceof ScriptImpl) { - return value.privateDoNotAccessOrElseWrappedScript; +export function unwrapScript(value: SafeScript): TrustedScript&string { + if (value instanceof ScriptImpl) { + const unwrapped = value.privateDoNotAccessOrElseWrappedScript; + return unwrapped as TrustedScript & string; } else { let message = ''; if (process.env.NODE_ENV !== 'production') {