diff --git a/package.json b/package.json index a364f9fa38b3..af8fd7315dd9 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "@types/selenium-webdriver": "4.0.15", "@types/sharp": "0.29.3", "@types/string-hash": "1.1.1", + "@types/trusted-types": "2.0.2", "@typescript-eslint/eslint-plugin": "4.29.1", "@typescript-eslint/parser": "4.29.1", "@vercel/fetch": "6.1.1", diff --git a/packages/next/client/route-loader.ts b/packages/next/client/route-loader.ts index d1198fc2a024..56d0a0200ba8 100644 --- a/packages/next/client/route-loader.ts +++ b/packages/next/client/route-loader.ts @@ -1,5 +1,6 @@ import type { ComponentType } from 'react' import getAssetPathFromRoute from '../shared/lib/router/utils/get-asset-path-from-route' +import { __unsafeCreateTrustedScriptURL } from './trusted-types' import { requestIdleCallback } from './request-idle-callback' // 3.8s was arbitrarily chosen as it's what https://web.dev/interactive @@ -135,7 +136,7 @@ export function isAssetError(err?: Error): boolean | undefined { } function appendScript( - src: string, + src: TrustedScriptURL | string, script?: HTMLScriptElement ): Promise { return new Promise((resolve, reject) => { @@ -154,7 +155,7 @@ function appendScript( // 3. Finally, set the source and inject into the DOM in case the child // must be appended for fetching to start. - script.src = src + script.src = src as string document.body.appendChild(script) }) } @@ -254,7 +255,7 @@ export function getMiddlewareManifest() { } interface RouteFiles { - scripts: string[] + scripts: (TrustedScriptURL | string)[] css: string[] } function getFilesForRoute( @@ -262,12 +263,12 @@ function getFilesForRoute( route: string ): Promise { if (process.env.NODE_ENV === 'development') { + const scriptUrl = + assetPrefix + + '/_next/static/chunks/pages' + + encodeURI(getAssetPathFromRoute(route, '.js')) return Promise.resolve({ - scripts: [ - assetPrefix + - '/_next/static/chunks/pages' + - encodeURI(getAssetPathFromRoute(route, '.js')), - ], + scripts: [__unsafeCreateTrustedScriptURL(scriptUrl)], // Styles are handled by `style-loader` in development: css: [], }) @@ -280,7 +281,9 @@ function getFilesForRoute( (entry) => assetPrefix + '/_next/' + encodeURI(entry) ) return { - scripts: allFiles.filter((v) => v.endsWith('.js')), + scripts: allFiles + .filter((v) => v.endsWith('.js')) + .map((v) => __unsafeCreateTrustedScriptURL(v)), css: allFiles.filter((v) => v.endsWith('.css')), } }) @@ -294,12 +297,14 @@ export function createRouteLoader(assetPrefix: string): RouteLoader { const routes: Map | RouteLoaderEntry> = new Map() - function maybeExecuteScript(src: string): Promise { + function maybeExecuteScript( + src: TrustedScriptURL | string + ): Promise { // With HMR we might need to "reload" scripts when they are // disposed and readded. Executing scripts twice has no functional // differences if (process.env.NODE_ENV !== 'development') { - let prom: Promise | undefined = loadedScripts.get(src) + let prom: Promise | undefined = loadedScripts.get(src.toString()) if (prom) { return prom } @@ -309,7 +314,7 @@ export function createRouteLoader(assetPrefix: string): RouteLoader { return Promise.resolve() } - loadedScripts.set(src, (prom = appendScript(src))) + loadedScripts.set(src.toString(), (prom = appendScript(src))) return prom } else { return appendScript(src) @@ -432,7 +437,9 @@ export function createRouteLoader(assetPrefix: string): RouteLoader { .then((output) => Promise.all( canPrefetch - ? output.scripts.map((script) => prefetchViaDom(script, 'script')) + ? output.scripts.map((script) => + prefetchViaDom(script.toString(), 'script') + ) : [] ) ) diff --git a/packages/next/client/trusted-types.ts b/packages/next/client/trusted-types.ts new file mode 100644 index 000000000000..9682635eb945 --- /dev/null +++ b/packages/next/client/trusted-types.ts @@ -0,0 +1,37 @@ +/** + * Stores the Trusted Types Policy. Starts as undefined and can be set to null + * if Trusted Types is not supported in the browser. + */ +let policy: TrustedTypePolicy | null | undefined + +/** + * Getter for the Trusted Types Policy. If it is undefined, it is instantiated + * here or set to null if Trusted Types is not supported in the browser. + */ +function getPolicy() { + if (typeof policy === 'undefined' && typeof window !== 'undefined') { + policy = + window.trustedTypes?.createPolicy('nextjs', { + createHTML: (input) => input, + createScript: (input) => input, + createScriptURL: (input) => input, + }) || null + } + + return policy +} + +/** + * Unsafely promote a string to a TrustedScriptURL, falling back to strings + * when Trusted Types are not available. + * 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 cause a browser to load and execute a resource, e.g. when + * assigning to script.src. + */ +export function __unsafeCreateTrustedScriptURL( + url: string +): TrustedScriptURL | string { + return getPolicy()?.createScriptURL(url) || url +} diff --git a/tsec-exemptions.json b/tsec-exemptions.json index ec33c3b673ea..acbcecdaa91c 100644 --- a/tsec-exemptions.json +++ b/tsec-exemptions.json @@ -9,10 +9,8 @@ "packages/next/client/script.tsx" ], "ban-script-content-assignments": ["packages/next/client/script.tsx"], - "ban-script-src-assignments": [ - "packages/next/client/route-loader.ts", - "packages/next/client/script.tsx" - ], + "ban-script-src-assignments": ["packages/next/client/script.tsx"], + "ban-trustedtypes-createpolicy": ["packages/next/client/trusted-types.ts"], "ban-window-stringfunctiondef": [ "packages/next/lib/recursive-delete.ts", "packages/next/client/dev/fouc.ts" diff --git a/yarn.lock b/yarn.lock index f3c7da9ae708..3cae0cb367a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5407,6 +5407,11 @@ dependencies: "@types/node" "*" +"@types/trusted-types@2.0.2": + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.2.tgz#fc25ad9943bcac11cceb8168db4f275e0e72e756" + integrity sha512-F5DIZ36YVLE+PN+Zwws4kJogq47hNgX3Nx6WyDJ3kcplxyke3XIzB8uK5n/Lpm1HBsbGzd6nmGehL8cPekP+Tg== + "@types/ua-parser-js@0.7.36": version "0.7.36" resolved "https://registry.yarnpkg.com/@types/ua-parser-js/-/ua-parser-js-0.7.36.tgz#9bd0b47f26b5a3151be21ba4ce9f5fa457c5f190"