Skip to content

Commit f523d4a

Browse files
authored
[backport]: config.allowedDevOrigins (#80410)
Backports: - #76880 - #77044 - #77053 - #77395 - #77414
1 parent ca92115 commit f523d4a

File tree

6 files changed

+479
-0
lines changed

6 files changed

+479
-0
lines changed

packages/next/src/server/config-schema.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ const zTurboRuleConfigItemOrShortcut: zod.ZodType<TurboRuleConfigItemOrShortcut>
125125

126126
export const configSchema: zod.ZodType<NextConfig> = z.lazy(() =>
127127
z.strictObject({
128+
allowedDevOrigins: z.array(z.string()).optional(),
128129
amp: z
129130
.object({
130131
canonicalBase: z.string().optional(),

packages/next/src/server/config-shared.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,8 @@ export type ExportPathMap = {
465465
* Read more: [Next.js Docs: `next.config.js`](https://nextjs.org/docs/api-reference/next.config.js/introduction)
466466
*/
467467
export interface NextConfig extends Record<string, any> {
468+
allowedDevOrigins?: string[]
469+
468470
exportPathMap?: (
469471
defaultMap: ExportPathMap,
470472
ctx: {
@@ -877,6 +879,7 @@ export const defaultConfig: NextConfig = {
877879
swcMinify: true,
878880
output: !!process.env.NEXT_PRIVATE_STANDALONE ? 'standalone' : undefined,
879881
modularizeImports: undefined,
882+
allowedDevOrigins: undefined,
880883
experimental: {
881884
multiZoneDraftMode: false,
882885
prerenderEarlyExit: false,

packages/next/src/server/lib/router-server.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { getHostname } from '../../shared/lib/get-hostname'
4242
import { detectDomainLocale } from '../../shared/lib/i18n/detect-domain-locale'
4343
import { normalizedAssetPrefix } from '../../shared/lib/normalized-asset-prefix'
4444
import { filterInternalHeaders } from './server-ipc/utils'
45+
import { blockCrossSite } from './router-utils/block-cross-site'
4546

4647
const debug = setupDebug('next:router-server:main')
4748
const isNextFont = (pathname: string | null) =>
@@ -301,6 +302,10 @@ export async function initialize(opts: {
301302

302303
// handle hot-reloader first
303304
if (developmentBundler) {
305+
if (blockCrossSite(req, res, config.allowedDevOrigins, opts.hostname)) {
306+
return
307+
}
308+
304309
const origUrl = req.url || '/'
305310

306311
if (config.basePath && pathHasPrefix(origUrl, config.basePath)) {
@@ -656,6 +661,11 @@ export async function initialize(opts: {
656661
})
657662

658663
if (opts.dev && developmentBundler && req.url) {
664+
if (
665+
blockCrossSite(req, socket, config.allowedDevOrigins, opts.hostname)
666+
) {
667+
return
668+
}
659669
const { basePath, assetPrefix } = config
660670

661671
let hmrPrefix = basePath
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import type { Duplex } from 'stream'
2+
import type { IncomingMessage, ServerResponse } from 'node:http'
3+
import { parseUrl } from '../../../lib/url'
4+
import { warnOnce } from '../../../build/output/log'
5+
import { isCsrfOriginAllowed } from '../../app-render/csrf-protection'
6+
7+
function warnOrBlockRequest(
8+
res: ServerResponse | Duplex,
9+
origin: string | undefined,
10+
mode: 'warn' | 'block'
11+
): boolean {
12+
const originString = origin ? `from ${origin}` : ''
13+
if (mode === 'warn') {
14+
warnOnce(
15+
`Cross origin request detected ${originString} to /_next/* resource. In a future major version of Next.js, you will need to explicitly configure "allowedDevOrigins" in next.config to allow this.\nRead more: https://nextjs.org/docs/app/api-reference/config/next-config-js/allowedDevOrigins`
16+
)
17+
18+
return false
19+
}
20+
21+
warnOnce(
22+
`Blocked cross-origin request ${originString} to /_next/* resource. To allow this, configure "allowedDevOrigins" in next.config\nRead more: https://nextjs.org/docs/app/api-reference/config/next-config-js/allowedDevOrigins`
23+
)
24+
25+
if ('statusCode' in res) {
26+
res.statusCode = 403
27+
}
28+
29+
res.end('Unauthorized')
30+
31+
return true
32+
}
33+
34+
function isInternalDevEndpoint(req: IncomingMessage): boolean {
35+
if (!req.url) return false
36+
37+
try {
38+
// TODO: We should standardize on a single prefix for this
39+
const isMiddlewareRequest = req.url.includes('/__nextjs')
40+
const isInternalAsset = req.url.includes('/_next')
41+
// Static media requests are excluded, as they might be loaded via CSS and would fail
42+
// CORS checks.
43+
const isIgnoredRequest =
44+
req.url.includes('/_next/image') ||
45+
req.url.includes('/_next/static/media')
46+
47+
return !isIgnoredRequest && (isInternalAsset || isMiddlewareRequest)
48+
} catch (err) {
49+
return false
50+
}
51+
}
52+
53+
export const blockCrossSite = (
54+
req: IncomingMessage,
55+
res: ServerResponse | Duplex,
56+
allowedDevOrigins: string[] | undefined,
57+
hostname: string | undefined
58+
): boolean => {
59+
// in the future, these will be blocked by default when allowed origins aren't configured.
60+
// for now, we warn when allowed origins aren't configured
61+
const mode = typeof allowedDevOrigins === 'undefined' ? 'warn' : 'block'
62+
63+
const allowedOrigins = [
64+
'*.localhost',
65+
'localhost',
66+
...(allowedDevOrigins || []),
67+
]
68+
if (hostname) {
69+
allowedOrigins.push(hostname)
70+
}
71+
72+
// only process internal URLs/middleware
73+
if (!isInternalDevEndpoint(req)) {
74+
return false
75+
}
76+
// block non-cors request from cross-site e.g. script tag on
77+
// different host
78+
if (
79+
req.headers['sec-fetch-mode'] === 'no-cors' &&
80+
req.headers['sec-fetch-site'] === 'cross-site'
81+
) {
82+
return warnOrBlockRequest(res, undefined, mode)
83+
}
84+
85+
// ensure websocket requests from allowed origin
86+
const rawOrigin = req.headers['origin']
87+
88+
if (rawOrigin) {
89+
const parsedOrigin = parseUrl(rawOrigin)
90+
91+
if (parsedOrigin) {
92+
const originLowerCase = parsedOrigin.hostname.toLowerCase()
93+
94+
if (!isCsrfOriginAllowed(originLowerCase, allowedOrigins)) {
95+
return warnOrBlockRequest(res, originLowerCase, mode)
96+
}
97+
}
98+
}
99+
100+
return false
101+
}

0 commit comments

Comments
 (0)