Skip to content

Commit 937254e

Browse files
authored
Add serverActions.allowedForwardedHosts option (#57529)
This new option specifies a list of host names that are considered safe, to accept as Server Action requests if they're different from the initial request origin. It can be very helpful when the hosted app has many layers of reverse proxies ahead. Closes #57397.
1 parent 1caa580 commit 937254e

File tree

20 files changed

+194
-71
lines changed

20 files changed

+194
-71
lines changed

packages/next-swc/crates/next-core/src/next_app/app_page_entry.rs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -155,13 +155,7 @@ async fn wrap_edge_page(
155155
// TODO(timneutkens): remove this
156156
let is_server_component = true;
157157

158-
// let server_actions_body_size_limit =
159-
// &next_config.experimental.server_actions.body_size_limit;
160-
let server_actions_body_size_limit = next_config
161-
.experimental
162-
.server_actions
163-
.as_ref()
164-
.and_then(|sa| sa.body_size_limit.as_ref());
158+
let server_actions = next_config.experimental.server_actions.as_ref();
165159

166160
let sri_enabled = !dev
167161
&& next_config
@@ -184,7 +178,7 @@ async fn wrap_edge_page(
184178
"nextConfig" => serde_json::to_string(next_config)?,
185179
"isServerComponent" => serde_json::Value::Bool(is_server_component).to_string(),
186180
"dev" => serde_json::Value::Bool(dev).to_string(),
187-
"serverActionsBodySizeLimit" => serde_json::to_string(&server_actions_body_size_limit)?
181+
"serverActions" => serde_json::to_string(&server_actions)?
188182
},
189183
indexmap! {
190184
"incrementalCacheHandler" => None,

packages/next/src/build/entries.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -417,8 +417,7 @@ export function getEdgeServerEntry(opts: {
417417
middlewareConfig: Buffer.from(
418418
JSON.stringify(opts.middlewareConfig || {})
419419
).toString('base64'),
420-
serverActionsBodySizeLimit:
421-
opts.config.experimental.serverActions?.bodySizeLimit,
420+
serverActions: opts.config.experimental.serverActions,
422421
}
423422

424423
return {

packages/next/src/build/templates/edge-ssr-app.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,12 @@ const error500Mod = null
2323
declare const sriEnabled: boolean
2424
declare const isServerComponent: boolean
2525
declare const dev: boolean
26-
declare const serverActionsBodySizeLimit: any
26+
declare const serverActions: any
2727
declare const nextConfig: NextConfigComplete
2828
// INJECT:sriEnabled
2929
// INJECT:isServerComponent
3030
// INJECT:dev
31-
// INJECT:serverActionsBodySizeLimit
31+
// INJECT:serverActions
3232
// INJECT:nextConfig
3333

3434
const maybeJSONParse = (str?: string) => (str ? JSON.parse(str) : undefined)
@@ -58,9 +58,7 @@ const render = getRender({
5858
reactLoadableManifest,
5959
clientReferenceManifest: isServerComponent ? rscManifest : null,
6060
serverActionsManifest: isServerComponent ? rscServerManifest : null,
61-
serverActionsBodySizeLimit: isServerComponent
62-
? serverActionsBodySizeLimit
63-
: undefined,
61+
serverActions: isServerComponent ? serverActions : undefined,
6462
subresourceIntegrityManifest,
6563
config: nextConfig,
6664
buildId: 'VAR_BUILD_ID',

packages/next/src/build/webpack/loaders/next-edge-ssr-loader/index.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ export type EdgeSSRLoaderQuery = {
2626
incrementalCacheHandlerPath?: string
2727
preferredRegion: string | string[] | undefined
2828
middlewareConfig: string
29-
serverActionsBodySizeLimit?: SizeLimit
29+
serverActions?: {
30+
bodySizeLimit?: SizeLimit
31+
allowedForwardedHosts?: string[]
32+
}
3033
}
3134

3235
/*
@@ -75,7 +78,7 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction<EdgeSSRLoaderQuery> =
7578
incrementalCacheHandlerPath,
7679
preferredRegion,
7780
middlewareConfig: middlewareConfigBase64,
78-
serverActionsBodySizeLimit,
81+
serverActions,
7982
} = this.getOptions()
8083

8184
const middlewareConfig: MiddlewareConfig = JSON.parse(
@@ -148,10 +151,10 @@ const edgeSSRLoader: webpack.LoaderDefinitionFunction<EdgeSSRLoaderQuery> =
148151
nextConfig: stringifiedConfig,
149152
isServerComponent: JSON.stringify(isServerComponent),
150153
dev: JSON.stringify(dev),
151-
serverActionsBodySizeLimit:
152-
typeof serverActionsBodySizeLimit === 'undefined'
154+
serverActions:
155+
typeof serverActions === 'undefined'
153156
? 'undefined'
154-
: JSON.stringify(serverActionsBodySizeLimit),
157+
: JSON.stringify(serverActions),
155158
},
156159
{
157160
incrementalCacheHandler: incrementalCacheHandlerPath ?? null,

packages/next/src/build/webpack/loaders/next-edge-ssr-loader/render.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export function getRender({
3434
clientReferenceManifest,
3535
subresourceIntegrityManifest,
3636
serverActionsManifest,
37-
serverActionsBodySizeLimit,
37+
serverActions,
3838
config,
3939
buildId,
4040
nextFontManifest,
@@ -55,7 +55,10 @@ export function getRender({
5555
subresourceIntegrityManifest?: Record<string, string>
5656
clientReferenceManifest?: ClientReferenceManifest
5757
serverActionsManifest?: any
58-
serverActionsBodySizeLimit?: SizeLimit
58+
serverActions?: {
59+
bodySizeLimit?: SizeLimit
60+
allowedForwardedHosts?: string[]
61+
}
5962
config: NextConfigComplete
6063
buildId: string
6164
nextFontManifest: NextFontManifest
@@ -87,7 +90,7 @@ export function getRender({
8790
supportsDynamicHTML: true,
8891
disableOptimizedLoading: true,
8992
serverActionsManifest,
90-
serverActionsBodySizeLimit,
93+
serverActions,
9194
nextFontManifest,
9295
},
9396
renderToHTML,

packages/next/src/build/webpack/plugins/flight-client-entry-plugin.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,13 @@ import {
3232
import { traverseModules, forEachEntryModule } from '../utils'
3333
import { normalizePathSep } from '../../../shared/lib/page-path/normalize-path-sep'
3434
import { getProxiedPluginState } from '../../build-context'
35-
import type { SizeLimit } from '../../../../types'
3635
import semver from 'next/dist/compiled/semver'
3736
import { generateRandomActionKeyRaw } from '../../../server/app-render/action-encryption-utils'
3837

3938
interface Options {
4039
dev: boolean
4140
appDir: string
4241
isEdgeServer: boolean
43-
serverActionsBodySizeLimit?: SizeLimit
4442
}
4543

4644
const PLUGIN_NAME = 'FlightClientEntryPlugin'
@@ -160,14 +158,12 @@ export class FlightClientEntryPlugin {
160158
dev: boolean
161159
appDir: string
162160
isEdgeServer: boolean
163-
serverActionsBodySizeLimit?: SizeLimit
164161
assetPrefix: string
165162

166163
constructor(options: Options) {
167164
this.dev = options.dev
168165
this.appDir = options.appDir
169166
this.isEdgeServer = options.isEdgeServer
170-
this.serverActionsBodySizeLimit = options.serverActionsBodySizeLimit
171167
this.assetPrefix = !this.dev && !this.isEdgeServer ? '../' : ''
172168
}
173169

packages/next/src/export/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -488,8 +488,7 @@ export async function exportAppImpl(
488488
optimizeFonts: nextConfig.optimizeFonts as FontConfig,
489489
largePageDataBytes: nextConfig.experimental.largePageDataBytes,
490490
serverComponents: options.hasAppDir,
491-
serverActionsBodySizeLimit:
492-
nextConfig.experimental.serverActions?.bodySizeLimit,
491+
serverActions: nextConfig.experimental.serverActions,
493492
nextFontManifest: require(join(
494493
distDir,
495494
'server',

packages/next/src/lib/turbopack-warning.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ const supportedTurbopackNextConfigOptions = [
7979
'experimental.scrollRestoration',
8080
'experimental.forceSwcTransforms',
8181
'experimental.serverActions.bodySizeLimit',
82+
'experimental.serverActions.allowedForwardedHosts',
8283
'experimental.memoryBasedWorkersCount',
8384
'experimental.clientRouterFilterRedirects',
8485
'experimental.webpackBuildWorker',

packages/next/src/server/app-render/action-handler.ts

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -216,7 +216,7 @@ export async function handleAction({
216216
generateFlight,
217217
staticGenerationStore,
218218
requestStore,
219-
serverActionsBodySizeLimit,
219+
serverActions,
220220
ctx,
221221
}: {
222222
req: IncomingMessage
@@ -232,7 +232,10 @@ export async function handleAction({
232232
generateFlight: GenerateFlight
233233
staticGenerationStore: StaticGenerationStore
234234
requestStore: RequestStore
235-
serverActionsBodySizeLimit?: SizeLimit
235+
serverActions?: {
236+
bodySizeLimit?: SizeLimit
237+
allowedForwardedHosts?: string[]
238+
}
236239
ctx: AppRenderContext
237240
}): Promise<
238241
| undefined
@@ -277,32 +280,41 @@ export async function handleAction({
277280
'Missing `origin` header from a forwarded Server Actions request.'
278281
)
279282
} else if (!host || originHostname !== host) {
280-
// This is an attack. We should not proceed the action.
281-
console.error(
282-
'`x-forwarded-host` and `host` headers do not match `origin` header from a forwarded Server Actions request. Aborting the action.'
283-
)
283+
// If the customer sets a list of allowed hosts, we'll allow the request.
284+
// These can be their reverse proxies or other safe hosts.
285+
if (
286+
typeof host === 'string' &&
287+
serverActions?.allowedForwardedHosts?.includes(host)
288+
) {
289+
// Ignore it
290+
} else {
291+
// This is an attack. We should not proceed the action.
292+
console.error(
293+
'`x-forwarded-host` and `host` headers do not match `origin` header from a forwarded Server Actions request. Aborting the action.'
294+
)
284295

285-
const error = new Error('Invalid Server Actions request.')
296+
const error = new Error('Invalid Server Actions request.')
286297

287-
if (isFetchAction) {
288-
res.statusCode = 500
289-
await Promise.all(staticGenerationStore.pendingRevalidates || [])
290-
const promise = Promise.reject(error)
291-
try {
292-
await promise
293-
} catch {}
298+
if (isFetchAction) {
299+
res.statusCode = 500
300+
await Promise.all(staticGenerationStore.pendingRevalidates || [])
301+
const promise = Promise.reject(error)
302+
try {
303+
await promise
304+
} catch {}
294305

295-
return {
296-
type: 'done',
297-
result: await generateFlight(ctx, {
298-
actionResult: promise,
299-
// if the page was not revalidated, we can skip the rendering the flight tree
300-
skipFlight: !staticGenerationStore.pathWasRevalidated,
301-
}),
306+
return {
307+
type: 'done',
308+
result: await generateFlight(ctx, {
309+
actionResult: promise,
310+
// if the page was not revalidated, we can skip the rendering the flight tree
311+
skipFlight: !staticGenerationStore.pathWasRevalidated,
312+
}),
313+
}
302314
}
303-
}
304315

305-
throw error
316+
throw error
317+
}
306318
}
307319

308320
// ensure we avoid caching server actions unexpectedly
@@ -421,15 +433,14 @@ export async function handleAction({
421433

422434
const actionData = Buffer.concat(chunks).toString('utf-8')
423435

424-
const limit = require('next/dist/compiled/bytes').parse(
425-
serverActionsBodySizeLimit ?? '1mb'
426-
)
436+
const readableLimit = serverActions?.bodySizeLimit ?? '1 MB'
437+
const limit = require('next/dist/compiled/bytes').parse(readableLimit)
427438

428439
if (actionData.length > limit) {
429440
const { ApiError } = require('../api-utils')
430441
throw new ApiError(
431442
413,
432-
`Body exceeded ${serverActionsBodySizeLimit} limit.
443+
`Body exceeded ${readableLimit} limit.
433444
To configure the body size limit for Server Actions, see: https://nextjs.org/docs/app/api-reference/server-actions#size-limitation`
434445
)
435446
}

packages/next/src/server/app-render/app-render.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -409,7 +409,7 @@ async function renderToHTMLOrFlightImpl(
409409
dev,
410410
nextFontManifest,
411411
supportsDynamicHTML,
412-
serverActionsBodySizeLimit,
412+
serverActions,
413413
buildId,
414414
appDirDevErrorLogger,
415415
assetPrefix = '',
@@ -955,7 +955,7 @@ async function renderToHTMLOrFlightImpl(
955955
generateFlight,
956956
staticGenerationStore: staticGenerationStore,
957957
requestStore: requestStore,
958-
serverActionsBodySizeLimit,
958+
serverActions,
959959
ctx,
960960
})
961961

0 commit comments

Comments
 (0)