Skip to content

Commit

Permalink
Fix route loader tsec violation
Browse files Browse the repository at this point in the history
  • Loading branch information
jgoping committed Feb 23, 2022
1 parent c74d765 commit b31e0a5
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 13 deletions.
1 change: 1 addition & 0 deletions package.json
Expand Up @@ -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",
Expand Down
33 changes: 20 additions & 13 deletions 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
Expand Down Expand Up @@ -135,7 +136,7 @@ export function isAssetError(err?: Error): boolean | undefined {
}

function appendScript(
src: string,
src: TrustedScriptURL | string,
script?: HTMLScriptElement
): Promise<unknown> {
return new Promise((resolve, reject) => {
Expand All @@ -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)
})
}
Expand Down Expand Up @@ -254,20 +255,20 @@ export function getMiddlewareManifest() {
}

interface RouteFiles {
scripts: string[]
scripts: (TrustedScriptURL | string)[]
css: string[]
}
function getFilesForRoute(
assetPrefix: string,
route: string
): Promise<RouteFiles> {
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: [],
})
Expand All @@ -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')),
}
})
Expand All @@ -294,12 +297,14 @@ export function createRouteLoader(assetPrefix: string): RouteLoader {
const routes: Map<string, Future<RouteLoaderEntry> | RouteLoaderEntry> =
new Map()

function maybeExecuteScript(src: string): Promise<unknown> {
function maybeExecuteScript(
src: TrustedScriptURL | string
): Promise<unknown> {
// 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<unknown> | undefined = loadedScripts.get(src)
let prom: Promise<unknown> | undefined = loadedScripts.get(src.toString())
if (prom) {
return prom
}
Expand All @@ -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)
Expand Down Expand Up @@ -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')
)
: []
)
)
Expand Down
37 changes: 37 additions & 0 deletions 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
}
5 changes: 5 additions & 0 deletions yarn.lock
Expand Up @@ -5297,6 +5297,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"
Expand Down

0 comments on commit b31e0a5

Please sign in to comment.