[prebuilt-skew-protection] feat: adding in automatic deploymentId#88012
[prebuilt-skew-protection] feat: adding in automatic deploymentId#88012
Conversation
|
Allow CI Workflow Run
Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer |
|
Allow CI Workflow Run
Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer |
There was a problem hiding this comment.
Additional Suggestions:
- The code uses
config.deploymentIddirectly in a template literal to create a cookie value, butdeploymentIdcan now be a function according to the schema changes, which would stringify to an invalid value like "[Function]".
View Details
📝 Patch Details
diff --git a/packages/next/src/lib/load-custom-routes.test.ts b/packages/next/src/lib/load-custom-routes.test.ts
index a4026d7180..b5bcf14a3b 100644
--- a/packages/next/src/lib/load-custom-routes.test.ts
+++ b/packages/next/src/lib/load-custom-routes.test.ts
@@ -173,4 +173,55 @@ describe('loadCustomRoutes', () => {
})
})
})
+
+ describe('useSkewCookie', () => {
+ it('adds Set-Cookie header when useSkewCookie is true and deploymentId is a string', async () => {
+ const customRoutes = await loadCustomRoutes({
+ experimental: {
+ useSkewCookie: true,
+ },
+ deploymentId: 'my-deployment-id',
+ })
+ expect(customRoutes.headers).toEqual([
+ {
+ source: '/:path*',
+ headers: [
+ {
+ key: 'Set-Cookie',
+ value: '__vdpl=my-deployment-id; Path=/; HttpOnly',
+ },
+ ],
+ },
+ ])
+ })
+
+ it('does not add Set-Cookie header when deploymentId is a function', async () => {
+ const customRoutes = await loadCustomRoutes({
+ experimental: {
+ useSkewCookie: true,
+ },
+ deploymentId: () => 'my-deployment-id',
+ })
+ expect(customRoutes.headers).toEqual([])
+ })
+
+ it('does not add Set-Cookie header when useSkewCookie is false', async () => {
+ const customRoutes = await loadCustomRoutes({
+ experimental: {
+ useSkewCookie: false,
+ },
+ deploymentId: 'my-deployment-id',
+ })
+ expect(customRoutes.headers).toEqual([])
+ })
+
+ it('does not add Set-Cookie header when deploymentId is undefined', async () => {
+ const customRoutes = await loadCustomRoutes({
+ experimental: {
+ useSkewCookie: true,
+ },
+ })
+ expect(customRoutes.headers).toEqual([])
+ })
+ })
})
diff --git a/packages/next/src/lib/load-custom-routes.ts b/packages/next/src/lib/load-custom-routes.ts
index 3ea01ce13a..552cafa083 100644
--- a/packages/next/src/lib/load-custom-routes.ts
+++ b/packages/next/src/lib/load-custom-routes.ts
@@ -725,7 +725,7 @@ export default async function loadCustomRoutes(
)
}
- if (config.experimental?.useSkewCookie && config.deploymentId) {
+ if (config.experimental?.useSkewCookie && config.deploymentId && typeof config.deploymentId === 'string') {
headers.unshift({
source: '/:path*',
headers: [
Analysis
Missing type check for deploymentId function in cookie header
What fails: loadCustomRoutes() uses config.deploymentId directly in a template literal to create a cookie value without verifying it's a string, but deploymentId is typed as string | (() => string | Promise<string>) | undefined. If a function is provided (as allowed by the config schema), it would stringify to the function source code instead of the actual ID.
How to reproduce: Create a next.config.js with:
module.exports = {
experimental: {
useSkewCookie: true,
},
deploymentId: () => 'my-deployment-id',
}Then run next build or next experimental-analyze. The Set-Cookie header would be set to __vdpl=() => 'my-deployment-id' instead of __vdpl=my-deployment-id.
Result: Invalid cookie value containing function source code rather than the deployment ID. This could occur in:
next experimental-analyze(analyze flow doesn't call generateDeploymentId)next dev(dev server can use function-type deploymentId from config)- Any custom code path calling
loadCustomRoutes()beforegenerateDeploymentId()conversion
Expected: Either skip the header if deploymentId is not a string (per TypeScript strict mode best practices), or ensure deploymentId is converted to a string before use.
Fix: Added typeof config.deploymentId === 'string' type guard to the condition to ensure the value is a string before using it in the template literal.
View Details
📝 Patch Details
diff --git a/packages/next/src/build/analyze/index.ts b/packages/next/src/build/analyze/index.ts
index 5723fb043c..24b3d0e167 100644
--- a/packages/next/src/build/analyze/index.ts
+++ b/packages/next/src/build/analyze/index.ts
@@ -9,6 +9,8 @@ import { PHASE_ANALYZE } from '../../shared/lib/constants'
import { turbopackAnalyze, type AnalyzeContext } from '../turbopack-analyze'
import { durationToString } from '../duration-to-string'
import { cp, writeFile, mkdir } from 'node:fs/promises'
+import { generateDeploymentId } from '../generate-deployment-id'
+import { nanoid } from 'next/dist/compiled/nanoid/index.cjs'
import {
collectAppFiles,
collectPagesFiles,
@@ -55,7 +57,11 @@ export default async function analyze({
reactProductionProfiling,
})
- process.env.NEXT_DEPLOYMENT_ID = config.deploymentId || ''
+ const deploymentId = await generateDeploymentId(
+ config.deploymentId,
+ nanoid
+ )
+ process.env.NEXT_DEPLOYMENT_ID = deploymentId
const distDir = path.join(dir, '.next')
const telemetry = new Telemetry({ distDir })
diff --git a/packages/next/src/build/turbopack-build/impl.ts b/packages/next/src/build/turbopack-build/impl.ts
index 4a6612641d..bd8c221ce0 100644
--- a/packages/next/src/build/turbopack-build/impl.ts
+++ b/packages/next/src/build/turbopack-build/impl.ts
@@ -12,6 +12,8 @@ import { TurbopackManifestLoader } from '../../shared/lib/turbopack/manifest-loa
import { promises as fs } from 'fs'
import { PHASE_PRODUCTION_BUILD } from '../../shared/lib/constants'
import loadConfig from '../../server/config'
+import { generateDeploymentId } from '../generate-deployment-id'
+import { nanoid } from 'next/dist/compiled/nanoid/index.cjs'
import { hasCustomExportOutput } from '../../export/utils'
import { Telemetry } from '../../telemetry/storage'
import { setGlobal } from '../../trace'
@@ -230,6 +232,14 @@ export async function workerMain(workerData: {
reactProductionProfiling: NextBuildContext.reactProductionProfiling,
}
))
+
+ // Evaluate deploymentId if it's a function
+ const deploymentId = await generateDeploymentId(
+ config.deploymentId,
+ nanoid
+ )
+ config.deploymentId = deploymentId
+
// Matches handling in build/index.ts
// https://github.com/vercel/next.js/blob/84f347fc86f4efc4ec9f13615c215e4b9fb6f8f0/packages/next/src/build/index.ts#L815-L818
// Ensures the `config.distDir` option is matched.
diff --git a/packages/next/src/build/webpack-build/impl.ts b/packages/next/src/build/webpack-build/impl.ts
index e332de299a..e35c3448a3 100644
--- a/packages/next/src/build/webpack-build/impl.ts
+++ b/packages/next/src/build/webpack-build/impl.ts
@@ -25,6 +25,8 @@ import {
} from '../build-context'
import { createEntrypoints } from '../entries'
import loadConfig from '../../server/config'
+import { generateDeploymentId } from '../generate-deployment-id'
+import { nanoid } from 'next/dist/compiled/nanoid/index.cjs'
import {
getTraceEvents,
initializeTraceState,
@@ -392,6 +394,14 @@ export async function workerMain(workerData: {
reactProductionProfiling: NextBuildContext.reactProductionProfiling,
}
))
+
+ // Evaluate deploymentId if it's a function
+ const deploymentId = await generateDeploymentId(
+ config.deploymentId,
+ nanoid
+ )
+ config.deploymentId = deploymentId
+
await installBindings(config.experimental?.useWasmBinary)
NextBuildContext.nextBuildSpan = trace(
`worker-main-${workerData.compilerName}`
diff --git a/packages/next/src/server/lib/router-server.ts b/packages/next/src/server/lib/router-server.ts
index 7c2360520f..3e0bf606a6 100644
--- a/packages/next/src/server/lib/router-server.ts
+++ b/packages/next/src/server/lib/router-server.ts
@@ -10,6 +10,8 @@ import '../require-hook'
import url from 'url'
import path from 'path'
import loadConfig from '../config'
+import { generateDeploymentId } from '../../build/generate-deployment-id'
+import { nanoid } from 'next/dist/compiled/nanoid/index.cjs'
import { serveStatic } from '../serve-static'
import setupDebug from 'next/dist/compiled/debug'
import * as Log from '../../build/output/log'
@@ -103,6 +105,13 @@ export async function initialize(opts: {
{ silent: false }
)
+ // Evaluate deploymentId if it's a function
+ const deploymentId = await generateDeploymentId(
+ config.deploymentId,
+ nanoid
+ )
+ config.deploymentId = deploymentId
+
let compress: ReturnType<typeof setupCompression> | undefined
if (config?.compress !== false) {
Analysis
Unhandled function-type deploymentId in analyze and runtime server initialization
What fails: When a user configures deploymentId as a function (per the Next.js config schema which allows deploymentId?: string | (() => string | Promise<string>)), the analyze command and production/dev server initialization do not evaluate the function, instead stringifying it to "[Function]" when assigning to process.env.NEXT_DEPLOYMENT_ID.
How to reproduce:
- Create a
next.config.jswith a function-based deploymentId:
module.exports = {
deploymentId: () => 'my-deployment-' + Date.now()
}- Run
next experimental-analyze- deploymentId becomes stringified - Start production server with
next start- deploymentId becomes stringified
Result: process.env.NEXT_DEPLOYMENT_ID is set to something like "() => 'my-deployment-' + Date.now()" instead of the actual evaluated deployment ID string
Expected: The function should be evaluated using generateDeploymentId() (which already exists and handles this case) to produce a proper string value
Root cause: While the build process correctly calls generateDeploymentId() to evaluate function-based deploymentId values (build/index.ts:938), the following paths do not:
build/analyze/index.ts:58- loads config and uses deploymentId directlyserver/lib/router-server.ts:102- loads config for dev/production server without evaluatingbuild/turbopack-build/impl.ts- worker process loads fresh config without evaluatingbuild/webpack-build/impl.ts- worker process loads fresh config without evaluating
All have been fixed to call generateDeploymentId() with the config's deploymentId value before use.
Reference: Next.js config schema allows deploymentId as function, with documented examples showing async functions like async () => { const res = await fetch(...); return res.text(); }
There was a problem hiding this comment.
Additional Suggestion:
The code at line 464 doesn't check if deploymentId is a string before using it, unlike the rest of the codebase. If a user provides a function for deploymentId in their configuration, it would be passed directly to process.env.NEXT_DEPLOYMENT_ID instead of being handled gracefully.
View Details
📝 Patch Details
diff --git a/packages/next/src/server/base-server.ts b/packages/next/src/server/base-server.ts
index 81c76b276e..3ecc77aa3d 100644
--- a/packages/next/src/server/base-server.ts
+++ b/packages/next/src/server/base-server.ts
@@ -461,8 +461,9 @@ export default abstract class Server<
} else {
let id = this.nextConfig.experimental.useSkewCookie
? ''
- : this.nextConfig.deploymentId || ''
-
+ : typeof this.nextConfig.deploymentId === 'string'
+ ? this.nextConfig.deploymentId
+ : ''
deploymentId = id
process.env.NEXT_DEPLOYMENT_ID = id
}
Analysis
Missing type check for deploymentId in BaseServer constructor
What fails: BaseServer constructor at line 464 does not check if deploymentId is a string before assigning it to process.env.NEXT_DEPLOYMENT_ID, allowing a function to be coerced to a string like "[Function: deploymentId]".
How to reproduce:
- Create a next.config.js with a function for deploymentId:
module.exports = {
deploymentId: () => 'my-deployment-id'
}- Start the dev server or production server without a prior build that would normalize the config
- Check
process.env.NEXT_DEPLOYMENT_ID
Result: process.env.NEXT_DEPLOYMENT_ID contains the string representation of the function (e.g., "() => 'my-deployment-id'") instead of an empty string or a properly evaluated ID.
Expected: Following the Next.js config documentation, deploymentId can be either a string or a function. When it's a function, it should be treated similarly to how other parts of the codebase handle it: with a typeof deploymentId === 'string' type check before using it.
The fix adds the proper type check to match the pattern used elsewhere in base-server.ts (lines 525-526, 534-535) and next-server.ts (lines 299-300, 734-735).
|
@brookemosby Is this a step towards allowing Next projects built locally/on a non-Vercel CI with If it's relevant to Vercel customers then we'd be happy to offer feedback, we're a Vercel enterprise customer that uses |
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
There was a problem hiding this comment.
Additional Suggestion:
deploymentId function not evaluated before string interpolation in cookie header
View Details
📝 Patch Details
diff --git a/packages/next/src/lib/load-custom-routes.ts b/packages/next/src/lib/load-custom-routes.ts
index 3ea01ce13a..2108696acd 100644
--- a/packages/next/src/lib/load-custom-routes.ts
+++ b/packages/next/src/lib/load-custom-routes.ts
@@ -726,12 +726,16 @@ export default async function loadCustomRoutes(
}
if (config.experimental?.useSkewCookie && config.deploymentId) {
+ const deploymentId =
+ typeof config.deploymentId === 'function'
+ ? config.deploymentId()
+ : config.deploymentId
headers.unshift({
source: '/:path*',
headers: [
{
key: 'Set-Cookie',
- value: `__vdpl=${config.deploymentId}; Path=/; HttpOnly`,
+ value: `__vdpl=${deploymentId}; Path=/; HttpOnly`,
},
],
})
Analysis
The code at packages/next/src/lib/load-custom-routes.ts line 734 directly interpolates config.deploymentId into a template literal for the Set-Cookie header value. The type of config.deploymentId is string | (() => string) | undefined. When a function is passed as the deploymentId, the template literal interpolation converts it to the string "() => string" or "[Function]" instead of executing the function to get the actual ID value.
This creates an invalid cookie header like __vdpl=() => 'git-hash-abc123'; Path=/; HttpOnly instead of the intended __vdpl=git-hash-abc123; Path=/; HttpOnly.
The fix checks if deploymentId is a function before using it in the template literal. If it is a function, it calls the function to get the string value. If it's already a string, it uses it directly. This ensures the cookie header always contains the actual deployment ID string value, not the function representation.
Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
…8496) Original [PR](#88012) has a bug that attempted to validate on environment variables. Edited function to not run validations on environment variables, only on the user provided deployment id. <!-- Thanks for opening a PR! Your contribution is much appreciated. To make sure your PR is handled as smoothly as possible we request that you follow the checklist sections below. Choose the right checklist for the change(s) that you're making: ## For Contributors ### Improving Documentation - Run `pnpm prettier-fix` to fix formatting issues before opening the PR. - Read the Docs Contribution Guide to ensure your contribution follows the docs guidelines: https://nextjs.org/docs/community/contribution-guide ### Fixing a bug - Related issues linked using `fixes #number` - Tests added. See: https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs - Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md ### Adding a feature - Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR. (A discussion must be opened, see https://github.com/vercel/next.js/discussions/new?category=ideas) - Related issues/discussions are linked using `fixes #number` - e2e tests added (https://github.com/vercel/next.js/blob/canary/contributing/core/testing.md#writing-tests-for-nextjs) - Documentation added - Telemetry added. In case of a feature if it's used or not. - Errors have a helpful link attached, see https://github.com/vercel/next.js/blob/canary/contributing.md ## For Maintainers - Minimal description (aim for explaining to someone not on the team to understand the PR) - When linking to a Slack thread, you might want to share details of the conclusion - Link both the Linear (Fixes NEXT-xxx) and the GitHub issues - Add review comments if necessary to explain to the reviewer the logic behind a change ### What? ### Why? ### How? Closes NEXT- Fixes # --> --------- Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com> Co-authored-by: JJ Kasper <jj@jjsweb.site>
https://linear.app/vercel/issue/FLOW-5530/nextjs-support-automatic-deploymentid-generation
So users can configure a deploymentId, to be used for skew protection with prebuilts