rsnext/packages/next/client/route-loader.ts

435 lines
13 KiB
TypeScript
Raw Normal View History

import type { ComponentType } from 'react'
import type { MiddlewareMatcher } from '../build/analysis/get-page-static-info'
import getAssetPathFromRoute from '../shared/lib/router/utils/get-asset-path-from-route'
Route Loader Trusted Types Violation Fix (#34730) Linked to issue #32209. ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [x] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation There is one tsec violation that is fixed in this PR: ### 1. ban-script-src-assignment: route-loader.ts XSS can occur with the line script.src = src in appendScript(src, script) if src can be controlled by a malicious user. From tracing through the code, it was determined that src comes from the function `getFilesForRoute(route)`. The behaviour of this function differs depending on the environment (development vs. production), but in both cases the function will construct strings that lead to valid file paths. These strings depend on two variables: `assetPrefix` and `route`, but due to the nature of the constructed strings it was determined that the scripts here are safe to use. Thus, the solution was to promote these strings to `TrustedScriptURL`s. This is the Trusted Types way of declaring that the script URL passed to the DOM sink is safe from DOM XSS attacks. To create a `TrustedScriptURL`, a policy needs to be created. This policy was put in its own file: `client/trusted-types.ts`. This policy has the name `nextjs`. If this name should be changed to something else, feel free to change it now. However, once it is released to the public and application developers begin using it, it may be harder to change the value since any application developers with a custom policy name allowlist would now need to update their `next.config.js` headers to allow this new name. The code was tested in a sample application to ensure it behaved as expected.
2022-05-04 01:22:08 +02:00
import { __unsafeCreateTrustedScriptURL } from './trusted-types'
import { requestIdleCallback } from './request-idle-callback'
// 3.8s was arbitrarily chosen as it's what https://web.dev/interactive
// considers as "Good" time-to-interactive. We must assume something went
// wrong beyond this point, and then fall-back to a full page transition to
// show the user something of value.
const MS_MAX_IDLE_DELAY = 3800
declare global {
interface Window {
__BUILD_MANIFEST?: Record<string, string[]>
__BUILD_MANIFEST_CB?: Function
__MIDDLEWARE_MATCHERS?: MiddlewareMatcher[]
__MIDDLEWARE_MANIFEST_CB?: Function
}
}
interface LoadedEntrypointSuccess {
component: ComponentType
exports: any
}
interface LoadedEntrypointFailure {
error: unknown
}
type RouteEntrypoint = LoadedEntrypointSuccess | LoadedEntrypointFailure
interface RouteStyleSheet {
href: string
content: string
}
interface LoadedRouteSuccess extends LoadedEntrypointSuccess {
styles: RouteStyleSheet[]
}
interface LoadedRouteFailure {
error: unknown
}
type RouteLoaderEntry = LoadedRouteSuccess | LoadedRouteFailure
interface Future<V> {
resolve: (entrypoint: V) => void
future: Promise<V>
}
function withFuture<T>(
key: string,
map: Map<string, Future<T> | T>,
generator?: () => Promise<T>
): Promise<T> {
let entry: Future<T> | T | undefined = map.get(key)
if (entry) {
if ('future' in entry) {
return entry.future
}
return Promise.resolve(entry)
}
let resolver: (entrypoint: T) => void
const prom: Promise<T> = new Promise<T>((resolve) => {
resolver = resolve
})
map.set(key, (entry = { resolve: resolver!, future: prom }))
return generator
? generator()
// eslint-disable-next-line no-sequences
.then((value) => (resolver(value), value))
.catch((err) => {
map.delete(key)
throw err
})
: prom
}
export interface RouteLoader {
whenEntrypoint(route: string): Promise<RouteEntrypoint>
onEntrypoint(route: string, execute: () => unknown): void
loadRoute(route: string, prefetch?: boolean): Promise<RouteLoaderEntry>
prefetch(route: string): Promise<void>
}
function hasPrefetch(link?: HTMLLinkElement): boolean {
try {
link = document.createElement('link')
return (
// detect IE11 since it supports prefetch but isn't detected
// with relList.support
(!!window.MSInputMethodContext && !!(document as any).documentMode) ||
link.relList.supports('prefetch')
)
} catch {
return false
}
}
const canPrefetch: boolean = hasPrefetch()
function prefetchViaDom(
href: string,
as: string,
link?: HTMLLinkElement
): Promise<any> {
return new Promise<void>((res, rej) => {
Fix #11107 - don't prefetch preloaded modules (#22818) This PR proposes a fix for https://github.com/vercel/next.js/issues/11107 (JS modules are loaded twice). A more detailed explanation of the investigation that led to this PR can be found in the issue's comments (https://github.com/vercel/next.js/issues/11107#issuecomment-791780168). ## Replicability To identify that the issue replicates on any given project, you need to 1. look at the network tab (first/clean load of site, so preferably ⌘+⇧+R on an incognito tab), 2. sort by "name", and filter requests by `mime-type:application/javascript` (selecting "JS" in the devtools filters will actually show all "script" types, but ignore all "javascript" types) 3. look for pairs of identical calls with one originating from initial HTML (`preload` of priority "high" originating from "(index)" or "([page name])") and another one from a script (`prefetch` of priority "lowest" originating from a .js file), where neither of the files is served from the cache. Here's a screenshot of an example of what to look for: <img width="601" alt="Screen Shot 2021-03-07 at 09 59 18" src="https://user-images.githubusercontent.com/1325721/110234627-cf1c6d00-7f2b-11eb-9cd7-749bf881ba56.png"> The issue was reproduced easily on the following projects: - On [nextjs.org](https://nextjs.org/) where duplicates add up to ~70kB of transferred javascript out of 470kB (14.9%). - On [vercel.com](https://vercel.com/) where duplicates add up to ~105kB of transferred javascript out of 557kB (18.8%). - On [tiktok.com](https://tiktok.com/en) where duplicates add up to ~514kB of transferred javascript out of 1556kB (33%). - In my own project using `"next": "^10.0.1"` (private repo) where duplicates add up to about 5% of total transferred javascript. - In the issue's comments, a developer reported a replication using `"^10.0.7"` on a [public repo](https://github.com/SidOfc/sidneyliebrand.io). ## Some information about the fix - Both `preload` and `prefetch` values for `<link rel="x">` behave similarly, with the difference being in network priority level (preload is high priority, prefetch is lowest priority). - Next.js uses `<link rel="preload">` in its initial HTML but then *only* uses `<link rel="prefetch">` for the rest of the lifetime of the page. - However, when Next.js detects that a script should be requested in advance, it only checks for matching `<link rel="prefetch">` and not `<link rel="preload">` (which have higher priority and are present earlier in the DOM, thus have a greater likelihood of being already loaded). This PR aims to fix that oversight. ## Potential issues (none AFAIK) As far as I can tell by looking through the codebase, **there is no downside** not to add a `prefetch` when a `preload` is already in the DOM. No other script looks for a `<link>` based on its `rel` attribute.
2021-09-19 19:51:04 +02:00
const selector = `
link[rel="prefetch"][href^="${href}"],
link[rel="preload"][href^="${href}"],
script[src^="${href}"]`
if (document.querySelector(selector)) {
return res()
}
link = document.createElement('link')
// The order of property assignment here is intentional:
if (as) link!.as = as
link!.rel = `prefetch`
link!.crossOrigin = process.env.__NEXT_CROSS_ORIGIN!
link!.onload = res as any
link!.onerror = rej
// `href` should always be last:
link!.href = href
document.head.appendChild(link)
})
}
const ASSET_LOAD_ERROR = Symbol('ASSET_LOAD_ERROR')
// TODO: unexport
export function markAssetError(err: Error): Error {
return Object.defineProperty(err, ASSET_LOAD_ERROR, {})
}
export function isAssetError(err?: Error): boolean | undefined {
return err && ASSET_LOAD_ERROR in err
}
function appendScript(
Route Loader Trusted Types Violation Fix (#34730) Linked to issue #32209. ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [x] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation There is one tsec violation that is fixed in this PR: ### 1. ban-script-src-assignment: route-loader.ts XSS can occur with the line script.src = src in appendScript(src, script) if src can be controlled by a malicious user. From tracing through the code, it was determined that src comes from the function `getFilesForRoute(route)`. The behaviour of this function differs depending on the environment (development vs. production), but in both cases the function will construct strings that lead to valid file paths. These strings depend on two variables: `assetPrefix` and `route`, but due to the nature of the constructed strings it was determined that the scripts here are safe to use. Thus, the solution was to promote these strings to `TrustedScriptURL`s. This is the Trusted Types way of declaring that the script URL passed to the DOM sink is safe from DOM XSS attacks. To create a `TrustedScriptURL`, a policy needs to be created. This policy was put in its own file: `client/trusted-types.ts`. This policy has the name `nextjs`. If this name should be changed to something else, feel free to change it now. However, once it is released to the public and application developers begin using it, it may be harder to change the value since any application developers with a custom policy name allowlist would now need to update their `next.config.js` headers to allow this new name. The code was tested in a sample application to ensure it behaved as expected.
2022-05-04 01:22:08 +02:00
src: TrustedScriptURL | string,
script?: HTMLScriptElement
): Promise<unknown> {
return new Promise((resolve, reject) => {
script = document.createElement('script')
// The order of property assignment here is intentional.
// 1. Setup success/failure hooks in case the browser synchronously
// executes when `src` is set.
script.onload = resolve
script.onerror = () =>
reject(markAssetError(new Error(`Failed to load script: ${src}`)))
// 2. Configure the cross-origin attribute before setting `src` in case the
// browser begins to fetch.
script.crossOrigin = process.env.__NEXT_CROSS_ORIGIN!
// 3. Finally, set the source and inject into the DOM in case the child
// must be appended for fetching to start.
Route Loader Trusted Types Violation Fix (#34730) Linked to issue #32209. ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [x] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation There is one tsec violation that is fixed in this PR: ### 1. ban-script-src-assignment: route-loader.ts XSS can occur with the line script.src = src in appendScript(src, script) if src can be controlled by a malicious user. From tracing through the code, it was determined that src comes from the function `getFilesForRoute(route)`. The behaviour of this function differs depending on the environment (development vs. production), but in both cases the function will construct strings that lead to valid file paths. These strings depend on two variables: `assetPrefix` and `route`, but due to the nature of the constructed strings it was determined that the scripts here are safe to use. Thus, the solution was to promote these strings to `TrustedScriptURL`s. This is the Trusted Types way of declaring that the script URL passed to the DOM sink is safe from DOM XSS attacks. To create a `TrustedScriptURL`, a policy needs to be created. This policy was put in its own file: `client/trusted-types.ts`. This policy has the name `nextjs`. If this name should be changed to something else, feel free to change it now. However, once it is released to the public and application developers begin using it, it may be harder to change the value since any application developers with a custom policy name allowlist would now need to update their `next.config.js` headers to allow this new name. The code was tested in a sample application to ensure it behaved as expected.
2022-05-04 01:22:08 +02:00
script.src = src as string
document.body.appendChild(script)
})
}
// We wait for pages to be built in dev before we start the route transition
// timeout to prevent an un-necessary hard navigation in development.
let devBuildPromise: Promise<void> | undefined
// Resolve a promise that times out after given amount of milliseconds.
function resolvePromiseWithTimeout<T>(
p: Promise<T>,
ms: number,
err: Error
): Promise<T> {
return new Promise((resolve, reject) => {
let cancelled = false
p.then((r) => {
// Resolved, cancel the timeout
cancelled = true
resolve(r)
}).catch(reject)
// We wrap these checks separately for better dead-code elimination in
// production bundles.
if (process.env.NODE_ENV === 'development') {
;(devBuildPromise || Promise.resolve()).then(() => {
requestIdleCallback(() =>
setTimeout(() => {
if (!cancelled) {
reject(err)
}
}, ms)
)
})
}
if (process.env.NODE_ENV !== 'development') {
requestIdleCallback(() =>
setTimeout(() => {
if (!cancelled) {
reject(err)
}
}, ms)
)
}
})
}
// TODO: stop exporting or cache the failure
// It'd be best to stop exporting this. It's an implementation detail. We're
2021-07-06 11:53:08 +02:00
// only exporting it for backwards compatibility with the `page-loader`.
// Only cache this response as a last resort if we cannot eliminate all other
// code branches that use the Build Manifest Callback and push them through
// the Route Loader interface.
export function getClientBuildManifest() {
if (self.__BUILD_MANIFEST) {
return Promise.resolve(self.__BUILD_MANIFEST)
}
const onBuildManifest = new Promise<Record<string, string[]>>((resolve) => {
// Mandatory because this is not concurrent safe:
const cb = self.__BUILD_MANIFEST_CB
self.__BUILD_MANIFEST_CB = () => {
resolve(self.__BUILD_MANIFEST!)
cb && cb()
}
})
return resolvePromiseWithTimeout(
onBuildManifest,
MS_MAX_IDLE_DELAY,
markAssetError(new Error('Failed to load client build manifest'))
)
}
interface RouteFiles {
Route Loader Trusted Types Violation Fix (#34730) Linked to issue #32209. ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [x] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation There is one tsec violation that is fixed in this PR: ### 1. ban-script-src-assignment: route-loader.ts XSS can occur with the line script.src = src in appendScript(src, script) if src can be controlled by a malicious user. From tracing through the code, it was determined that src comes from the function `getFilesForRoute(route)`. The behaviour of this function differs depending on the environment (development vs. production), but in both cases the function will construct strings that lead to valid file paths. These strings depend on two variables: `assetPrefix` and `route`, but due to the nature of the constructed strings it was determined that the scripts here are safe to use. Thus, the solution was to promote these strings to `TrustedScriptURL`s. This is the Trusted Types way of declaring that the script URL passed to the DOM sink is safe from DOM XSS attacks. To create a `TrustedScriptURL`, a policy needs to be created. This policy was put in its own file: `client/trusted-types.ts`. This policy has the name `nextjs`. If this name should be changed to something else, feel free to change it now. However, once it is released to the public and application developers begin using it, it may be harder to change the value since any application developers with a custom policy name allowlist would now need to update their `next.config.js` headers to allow this new name. The code was tested in a sample application to ensure it behaved as expected.
2022-05-04 01:22:08 +02:00
scripts: (TrustedScriptURL | string)[]
css: string[]
}
function getFilesForRoute(
assetPrefix: string,
route: string
): Promise<RouteFiles> {
if (process.env.NODE_ENV === 'development') {
Route Loader Trusted Types Violation Fix (#34730) Linked to issue #32209. ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [x] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation There is one tsec violation that is fixed in this PR: ### 1. ban-script-src-assignment: route-loader.ts XSS can occur with the line script.src = src in appendScript(src, script) if src can be controlled by a malicious user. From tracing through the code, it was determined that src comes from the function `getFilesForRoute(route)`. The behaviour of this function differs depending on the environment (development vs. production), but in both cases the function will construct strings that lead to valid file paths. These strings depend on two variables: `assetPrefix` and `route`, but due to the nature of the constructed strings it was determined that the scripts here are safe to use. Thus, the solution was to promote these strings to `TrustedScriptURL`s. This is the Trusted Types way of declaring that the script URL passed to the DOM sink is safe from DOM XSS attacks. To create a `TrustedScriptURL`, a policy needs to be created. This policy was put in its own file: `client/trusted-types.ts`. This policy has the name `nextjs`. If this name should be changed to something else, feel free to change it now. However, once it is released to the public and application developers begin using it, it may be harder to change the value since any application developers with a custom policy name allowlist would now need to update their `next.config.js` headers to allow this new name. The code was tested in a sample application to ensure it behaved as expected.
2022-05-04 01:22:08 +02:00
const scriptUrl =
assetPrefix +
'/_next/static/chunks/pages' +
encodeURI(getAssetPathFromRoute(route, '.js'))
return Promise.resolve({
Route Loader Trusted Types Violation Fix (#34730) Linked to issue #32209. ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [x] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation There is one tsec violation that is fixed in this PR: ### 1. ban-script-src-assignment: route-loader.ts XSS can occur with the line script.src = src in appendScript(src, script) if src can be controlled by a malicious user. From tracing through the code, it was determined that src comes from the function `getFilesForRoute(route)`. The behaviour of this function differs depending on the environment (development vs. production), but in both cases the function will construct strings that lead to valid file paths. These strings depend on two variables: `assetPrefix` and `route`, but due to the nature of the constructed strings it was determined that the scripts here are safe to use. Thus, the solution was to promote these strings to `TrustedScriptURL`s. This is the Trusted Types way of declaring that the script URL passed to the DOM sink is safe from DOM XSS attacks. To create a `TrustedScriptURL`, a policy needs to be created. This policy was put in its own file: `client/trusted-types.ts`. This policy has the name `nextjs`. If this name should be changed to something else, feel free to change it now. However, once it is released to the public and application developers begin using it, it may be harder to change the value since any application developers with a custom policy name allowlist would now need to update their `next.config.js` headers to allow this new name. The code was tested in a sample application to ensure it behaved as expected.
2022-05-04 01:22:08 +02:00
scripts: [__unsafeCreateTrustedScriptURL(scriptUrl)],
// Styles are handled by `style-loader` in development:
css: [],
})
}
return getClientBuildManifest().then((manifest) => {
if (!(route in manifest)) {
throw markAssetError(new Error(`Failed to lookup route: ${route}`))
}
const allFiles = manifest[route].map(
(entry) => assetPrefix + '/_next/' + encodeURI(entry)
)
return {
Route Loader Trusted Types Violation Fix (#34730) Linked to issue #32209. ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [x] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation There is one tsec violation that is fixed in this PR: ### 1. ban-script-src-assignment: route-loader.ts XSS can occur with the line script.src = src in appendScript(src, script) if src can be controlled by a malicious user. From tracing through the code, it was determined that src comes from the function `getFilesForRoute(route)`. The behaviour of this function differs depending on the environment (development vs. production), but in both cases the function will construct strings that lead to valid file paths. These strings depend on two variables: `assetPrefix` and `route`, but due to the nature of the constructed strings it was determined that the scripts here are safe to use. Thus, the solution was to promote these strings to `TrustedScriptURL`s. This is the Trusted Types way of declaring that the script URL passed to the DOM sink is safe from DOM XSS attacks. To create a `TrustedScriptURL`, a policy needs to be created. This policy was put in its own file: `client/trusted-types.ts`. This policy has the name `nextjs`. If this name should be changed to something else, feel free to change it now. However, once it is released to the public and application developers begin using it, it may be harder to change the value since any application developers with a custom policy name allowlist would now need to update their `next.config.js` headers to allow this new name. The code was tested in a sample application to ensure it behaved as expected.
2022-05-04 01:22:08 +02:00
scripts: allFiles
.filter((v) => v.endsWith('.js'))
.map((v) => __unsafeCreateTrustedScriptURL(v)),
css: allFiles.filter((v) => v.endsWith('.css')),
}
})
}
export function createRouteLoader(assetPrefix: string): RouteLoader {
const entrypoints: Map<string, Future<RouteEntrypoint> | RouteEntrypoint> =
new Map()
const loadedScripts: Map<string, Promise<unknown>> = new Map()
const styleSheets: Map<string, Promise<RouteStyleSheet>> = new Map()
const routes: Map<string, Future<RouteLoaderEntry> | RouteLoaderEntry> =
new Map()
Route Loader Trusted Types Violation Fix (#34730) Linked to issue #32209. ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [x] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation There is one tsec violation that is fixed in this PR: ### 1. ban-script-src-assignment: route-loader.ts XSS can occur with the line script.src = src in appendScript(src, script) if src can be controlled by a malicious user. From tracing through the code, it was determined that src comes from the function `getFilesForRoute(route)`. The behaviour of this function differs depending on the environment (development vs. production), but in both cases the function will construct strings that lead to valid file paths. These strings depend on two variables: `assetPrefix` and `route`, but due to the nature of the constructed strings it was determined that the scripts here are safe to use. Thus, the solution was to promote these strings to `TrustedScriptURL`s. This is the Trusted Types way of declaring that the script URL passed to the DOM sink is safe from DOM XSS attacks. To create a `TrustedScriptURL`, a policy needs to be created. This policy was put in its own file: `client/trusted-types.ts`. This policy has the name `nextjs`. If this name should be changed to something else, feel free to change it now. However, once it is released to the public and application developers begin using it, it may be harder to change the value since any application developers with a custom policy name allowlist would now need to update their `next.config.js` headers to allow this new name. The code was tested in a sample application to ensure it behaved as expected.
2022-05-04 01:22:08 +02:00
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') {
Route Loader Trusted Types Violation Fix (#34730) Linked to issue #32209. ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [x] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation There is one tsec violation that is fixed in this PR: ### 1. ban-script-src-assignment: route-loader.ts XSS can occur with the line script.src = src in appendScript(src, script) if src can be controlled by a malicious user. From tracing through the code, it was determined that src comes from the function `getFilesForRoute(route)`. The behaviour of this function differs depending on the environment (development vs. production), but in both cases the function will construct strings that lead to valid file paths. These strings depend on two variables: `assetPrefix` and `route`, but due to the nature of the constructed strings it was determined that the scripts here are safe to use. Thus, the solution was to promote these strings to `TrustedScriptURL`s. This is the Trusted Types way of declaring that the script URL passed to the DOM sink is safe from DOM XSS attacks. To create a `TrustedScriptURL`, a policy needs to be created. This policy was put in its own file: `client/trusted-types.ts`. This policy has the name `nextjs`. If this name should be changed to something else, feel free to change it now. However, once it is released to the public and application developers begin using it, it may be harder to change the value since any application developers with a custom policy name allowlist would now need to update their `next.config.js` headers to allow this new name. The code was tested in a sample application to ensure it behaved as expected.
2022-05-04 01:22:08 +02:00
let prom: Promise<unknown> | undefined = loadedScripts.get(src.toString())
if (prom) {
return prom
}
// Skip executing script if it's already in the DOM:
if (document.querySelector(`script[src^="${src}"]`)) {
return Promise.resolve()
}
Route Loader Trusted Types Violation Fix (#34730) Linked to issue #32209. ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [x] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation There is one tsec violation that is fixed in this PR: ### 1. ban-script-src-assignment: route-loader.ts XSS can occur with the line script.src = src in appendScript(src, script) if src can be controlled by a malicious user. From tracing through the code, it was determined that src comes from the function `getFilesForRoute(route)`. The behaviour of this function differs depending on the environment (development vs. production), but in both cases the function will construct strings that lead to valid file paths. These strings depend on two variables: `assetPrefix` and `route`, but due to the nature of the constructed strings it was determined that the scripts here are safe to use. Thus, the solution was to promote these strings to `TrustedScriptURL`s. This is the Trusted Types way of declaring that the script URL passed to the DOM sink is safe from DOM XSS attacks. To create a `TrustedScriptURL`, a policy needs to be created. This policy was put in its own file: `client/trusted-types.ts`. This policy has the name `nextjs`. If this name should be changed to something else, feel free to change it now. However, once it is released to the public and application developers begin using it, it may be harder to change the value since any application developers with a custom policy name allowlist would now need to update their `next.config.js` headers to allow this new name. The code was tested in a sample application to ensure it behaved as expected.
2022-05-04 01:22:08 +02:00
loadedScripts.set(src.toString(), (prom = appendScript(src)))
return prom
} else {
return appendScript(src)
}
}
function fetchStyleSheet(href: string): Promise<RouteStyleSheet> {
let prom: Promise<RouteStyleSheet> | undefined = styleSheets.get(href)
if (prom) {
return prom
}
styleSheets.set(
href,
(prom = fetch(href)
.then((res) => {
if (!res.ok) {
throw new Error(`Failed to load stylesheet: ${href}`)
}
return res.text().then((text) => ({ href: href, content: text }))
})
.catch((err) => {
throw markAssetError(err)
}))
)
return prom
}
return {
whenEntrypoint(route: string) {
return withFuture(route, entrypoints)
},
onEntrypoint(route: string, execute: undefined | (() => unknown)) {
;(execute
? Promise.resolve()
.then(() => execute())
.then(
(exports: any) => ({
component: (exports && exports.default) || exports,
exports: exports,
}),
(err) => ({ error: err })
)
: Promise.resolve(undefined)
).then((input: RouteEntrypoint | undefined) => {
const old = entrypoints.get(route)
if (old && 'resolve' in old) {
if (input) {
entrypoints.set(route, input)
old.resolve(input)
}
} else {
if (input) {
entrypoints.set(route, input)
} else {
entrypoints.delete(route)
}
// when this entrypoint has been resolved before
// the route is outdated and we want to invalidate
// this cache entry
routes.delete(route)
}
})
},
loadRoute(route: string, prefetch?: boolean) {
return withFuture<RouteLoaderEntry>(route, routes, () => {
let devBuildPromiseResolve: () => void
if (process.env.NODE_ENV === 'development') {
devBuildPromise = new Promise<void>((resolve) => {
devBuildPromiseResolve = resolve
})
}
return resolvePromiseWithTimeout(
getFilesForRoute(assetPrefix, route)
.then(({ scripts, css }) => {
return Promise.all([
entrypoints.has(route)
? []
: Promise.all(scripts.map(maybeExecuteScript)),
Promise.all(css.map(fetchStyleSheet)),
] as const)
})
.then((res) => {
return this.whenEntrypoint(route).then((entrypoint) => ({
entrypoint,
styles: res[1],
}))
}),
MS_MAX_IDLE_DELAY,
markAssetError(new Error(`Route did not complete loading: ${route}`))
)
.then(({ entrypoint, styles }) => {
const res: RouteLoaderEntry = Object.assign<
{ styles: RouteStyleSheet[] },
RouteEntrypoint
>({ styles: styles! }, entrypoint)
return 'error' in entrypoint ? entrypoint : res
})
.catch((err) => {
if (prefetch) {
// we don't want to cache errors during prefetch
throw err
}
return { error: err }
})
.finally(() => devBuildPromiseResolve?.())
})
},
prefetch(route: string): Promise<void> {
// https://github.com/GoogleChromeLabs/quicklink/blob/453a661fa1fa940e2d2e044452398e38c67a98fb/src/index.mjs#L115-L118
// License: Apache 2.0
let cn
if ((cn = (navigator as any).connection)) {
// Don't prefetch if using 2G or if Save-Data is enabled.
if (cn.saveData || /2g/.test(cn.effectiveType)) return Promise.resolve()
}
return getFilesForRoute(assetPrefix, route)
.then((output) =>
Promise.all(
canPrefetch
Route Loader Trusted Types Violation Fix (#34730) Linked to issue #32209. ## Feature - [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. - [x] Related issues linked using `fixes #number` - [ ] Integration tests added - [ ] Documentation added - [ ] Telemetry added. In case of a feature if it's used or not. - [ ] Errors have helpful link attached, see `contributing.md` ## Documentation There is one tsec violation that is fixed in this PR: ### 1. ban-script-src-assignment: route-loader.ts XSS can occur with the line script.src = src in appendScript(src, script) if src can be controlled by a malicious user. From tracing through the code, it was determined that src comes from the function `getFilesForRoute(route)`. The behaviour of this function differs depending on the environment (development vs. production), but in both cases the function will construct strings that lead to valid file paths. These strings depend on two variables: `assetPrefix` and `route`, but due to the nature of the constructed strings it was determined that the scripts here are safe to use. Thus, the solution was to promote these strings to `TrustedScriptURL`s. This is the Trusted Types way of declaring that the script URL passed to the DOM sink is safe from DOM XSS attacks. To create a `TrustedScriptURL`, a policy needs to be created. This policy was put in its own file: `client/trusted-types.ts`. This policy has the name `nextjs`. If this name should be changed to something else, feel free to change it now. However, once it is released to the public and application developers begin using it, it may be harder to change the value since any application developers with a custom policy name allowlist would now need to update their `next.config.js` headers to allow this new name. The code was tested in a sample application to ensure it behaved as expected.
2022-05-04 01:22:08 +02:00
? output.scripts.map((script) =>
prefetchViaDom(script.toString(), 'script')
)
: []
)
)
.then(() => {
requestIdleCallback(() => this.loadRoute(route, true).catch(() => {}))
})
.catch(
// swallow prefetch errors
() => {}
)
},
}
}