Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/next/src/server/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ const zTurboRuleConfigItemOrShortcut: zod.ZodType<TurboRuleConfigItemOrShortcut>

export const configSchema: zod.ZodType<NextConfig> = z.lazy(() =>
z.strictObject({
allowedDevOrigins: z.array(z.string()).optional(),
amp: z
.object({
canonicalBase: z.string().optional(),
Expand Down
3 changes: 3 additions & 0 deletions packages/next/src/server/config-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,8 @@ export type ExportPathMap = {
* Read more: [Next.js Docs: `next.config.js`](https://nextjs.org/docs/api-reference/next.config.js/introduction)
*/
export interface NextConfig extends Record<string, any> {
allowedDevOrigins?: string[]

exportPathMap?: (
defaultMap: ExportPathMap,
ctx: {
Expand Down Expand Up @@ -877,6 +879,7 @@ export const defaultConfig: NextConfig = {
swcMinify: true,
output: !!process.env.NEXT_PRIVATE_STANDALONE ? 'standalone' : undefined,
modularizeImports: undefined,
allowedDevOrigins: undefined,
experimental: {
multiZoneDraftMode: false,
prerenderEarlyExit: false,
Expand Down
10 changes: 10 additions & 0 deletions packages/next/src/server/lib/router-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { getHostname } from '../../shared/lib/get-hostname'
import { detectDomainLocale } from '../../shared/lib/i18n/detect-domain-locale'
import { normalizedAssetPrefix } from '../../shared/lib/normalized-asset-prefix'
import { filterInternalHeaders } from './server-ipc/utils'
import { blockCrossSite } from './router-utils/block-cross-site'

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

// handle hot-reloader first
if (developmentBundler) {
if (blockCrossSite(req, res, config.allowedDevOrigins, opts.hostname)) {
return
}

const origUrl = req.url || '/'

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

if (opts.dev && developmentBundler && req.url) {
if (
blockCrossSite(req, socket, config.allowedDevOrigins, opts.hostname)
) {
return
}
const { basePath, assetPrefix } = config

let hmrPrefix = basePath
Expand Down
101 changes: 101 additions & 0 deletions packages/next/src/server/lib/router-utils/block-cross-site.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import type { Duplex } from 'stream'
import type { IncomingMessage, ServerResponse } from 'node:http'
import { parseUrl } from '../../../lib/url'
import { warnOnce } from '../../../build/output/log'
import { isCsrfOriginAllowed } from '../../app-render/csrf-protection'

function warnOrBlockRequest(
res: ServerResponse | Duplex,
origin: string | undefined,
mode: 'warn' | 'block'
): boolean {
const originString = origin ? `from ${origin}` : ''
if (mode === 'warn') {
warnOnce(
`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`
)

return false
}

warnOnce(
`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`
)

if ('statusCode' in res) {
res.statusCode = 403
}

res.end('Unauthorized')

return true
}

function isInternalDevEndpoint(req: IncomingMessage): boolean {
if (!req.url) return false

try {
// TODO: We should standardize on a single prefix for this
const isMiddlewareRequest = req.url.includes('/__nextjs')
const isInternalAsset = req.url.includes('/_next')
// Static media requests are excluded, as they might be loaded via CSS and would fail
// CORS checks.
const isIgnoredRequest =
req.url.includes('/_next/image') ||
req.url.includes('/_next/static/media')

return !isIgnoredRequest && (isInternalAsset || isMiddlewareRequest)
} catch (err) {
return false
}
}

export const blockCrossSite = (
req: IncomingMessage,
res: ServerResponse | Duplex,
allowedDevOrigins: string[] | undefined,
hostname: string | undefined
): boolean => {
// in the future, these will be blocked by default when allowed origins aren't configured.
// for now, we warn when allowed origins aren't configured
const mode = typeof allowedDevOrigins === 'undefined' ? 'warn' : 'block'

const allowedOrigins = [
'*.localhost',
'localhost',
...(allowedDevOrigins || []),
]
if (hostname) {
allowedOrigins.push(hostname)
}

// only process internal URLs/middleware
if (!isInternalDevEndpoint(req)) {
return false
}
// block non-cors request from cross-site e.g. script tag on
// different host
if (
req.headers['sec-fetch-mode'] === 'no-cors' &&
req.headers['sec-fetch-site'] === 'cross-site'
) {
return warnOrBlockRequest(res, undefined, mode)
}

// ensure websocket requests from allowed origin
const rawOrigin = req.headers['origin']

if (rawOrigin) {
const parsedOrigin = parseUrl(rawOrigin)

if (parsedOrigin) {
const originLowerCase = parsedOrigin.hostname.toLowerCase()

if (!isCsrfOriginAllowed(originLowerCase, allowedOrigins)) {
return warnOrBlockRequest(res, originLowerCase, mode)
}
}
}

return false
}
Loading
Loading