diff --git a/docs/01-app/04-api-reference/05-config/01-next-config-js/allowedDevOrigins.mdx b/docs/01-app/04-api-reference/05-config/01-next-config-js/allowedDevOrigins.mdx
index 680fc1ec2fbb0..028f426b11319 100644
--- a/docs/01-app/04-api-reference/05-config/01-next-config-js/allowedDevOrigins.mdx
+++ b/docs/01-app/04-api-reference/05-config/01-next-config-js/allowedDevOrigins.mdx
@@ -5,6 +5,8 @@ description: Use `allowedDevOrigins` to configure additional origins that can re
{/* The content of this doc is shared between the app and pages router. You can use the `Content` component to add content that is specific to the Pages Router. Any shared content should not be wrapped in a component. */}
+Next.js does not automatically block cross-origin requests during development, but will block by default in a future major version of Next.js to prevent unauthorized requesting of internal assets/endpoints that are available in development mode.
+
To configure a Next.js application to allow requests from origins other than the hostname the server was initialized with (`localhost` by default) you can use the `allowedDevOrigins` config option.
`allowedDevOrigins` allows you to set additional origins that can be used in development mode. For example, to use `local-origin.dev` instead of only `localhost`, open `next.config.js` and add the `allowedDevOrigins` config:
@@ -14,5 +16,3 @@ module.exports = {
allowedDevOrigins: ['local-origin.dev', '*.local-origin.dev'],
}
```
-
-Cross-origin requests are blocked by default to prevent unauthorized requesting of internal assets/endpoints which are available in development mode. This behavior is similar to other dev servers like `webpack-dev-middleware` to ensure the same protection.
diff --git a/packages/next/src/server/config-shared.ts b/packages/next/src/server/config-shared.ts
index 91920133420fd..4fb7dac5e8cf2 100644
--- a/packages/next/src/server/config-shared.ts
+++ b/packages/next/src/server/config-shared.ts
@@ -1144,7 +1144,7 @@ export const defaultConfig: NextConfig = {
output: !!process.env.NEXT_PRIVATE_STANDALONE ? 'standalone' : undefined,
modularizeImports: undefined,
outputFileTracingRoot: process.env.NEXT_PRIVATE_OUTPUT_TRACE_ROOT || '',
- allowedDevOrigins: [],
+ allowedDevOrigins: undefined,
experimental: {
nodeMiddleware: false,
cacheLife: {
diff --git a/packages/next/src/server/lib/router-server.ts b/packages/next/src/server/lib/router-server.ts
index 7cb2b720fbbb4..e39c71e66a619 100644
--- a/packages/next/src/server/lib/router-server.ts
+++ b/packages/next/src/server/lib/router-server.ts
@@ -172,15 +172,6 @@ export async function initialize(opts: {
;(globalThis as any)[Symbol.for('@next/middleware-subrequest-id')] =
middlewareSubrequestId
- const allowedOrigins = [
- '*.localhost',
- 'localhost',
- ...(config.allowedDevOrigins || []),
- ]
- if (opts.hostname) {
- allowedOrigins.push(opts.hostname)
- }
-
const requestHandlerImpl: WorkerRequestHandler = async (req, res) => {
// internal headers should not be honored by the request handler
if (!process.env.NEXT_PRIVATE_TEST_HEADERS) {
@@ -332,7 +323,15 @@ export async function initialize(opts: {
// handle hot-reloader first
if (developmentBundler) {
- if (blockCrossSite(req, res, allowedOrigins, `${opts.port}`)) {
+ if (
+ blockCrossSite(
+ req,
+ res,
+ config.allowedDevOrigins,
+ opts.hostname,
+ `${opts.port}`
+ )
+ ) {
return
}
const origUrl = req.url || '/'
@@ -698,7 +697,15 @@ export async function initialize(opts: {
})
if (opts.dev && developmentBundler && req.url) {
- if (blockCrossSite(req, socket, allowedOrigins, `${opts.port}`)) {
+ if (
+ blockCrossSite(
+ req,
+ socket,
+ config.allowedDevOrigins,
+ opts.hostname,
+ `${opts.port}`
+ )
+ ) {
return
}
const { basePath, assetPrefix } = config
diff --git a/packages/next/src/server/lib/router-utils/block-cross-site.ts b/packages/next/src/server/lib/router-utils/block-cross-site.ts
index 4287d290afbab..ccdefa728cbff 100644
--- a/packages/next/src/server/lib/router-utils/block-cross-site.ts
+++ b/packages/next/src/server/lib/router-utils/block-cross-site.ts
@@ -5,13 +5,54 @@ import net from 'net'
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
+}
+
export const blockCrossSite = (
req: IncomingMessage,
res: ServerResponse | Duplex,
- allowedOrigins: string[],
+ allowedDevOrigins: string[] | undefined,
+ hostname: string | undefined,
activePort: string
): boolean => {
- // only process _next URLs
+ // 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 _next URLs when
if (!req.url?.includes('/_next')) {
return false
}
@@ -21,14 +62,7 @@ export const blockCrossSite = (
req.headers['sec-fetch-mode'] === 'no-cors' &&
req.headers['sec-fetch-site'] === 'cross-site'
) {
- if ('statusCode' in res) {
- res.statusCode = 403
- }
- res.end('Unauthorized')
- warnOnce(
- `Blocked cross-origin request to /_next/*. Cross-site requests are blocked in "no-cors" mode.`
- )
- return true
+ return warnOrBlockRequest(res, undefined, mode)
}
// ensure websocket requests from allowed origin
@@ -49,14 +83,7 @@ export const blockCrossSite = (
!(isIpRequest && isMatchingPort) &&
!isCsrfOriginAllowed(originLowerCase, allowedOrigins)
) {
- if ('statusCode' in res) {
- res.statusCode = 403
- }
- res.end('Unauthorized')
- warnOnce(
- `Blocked cross-origin request from ${originLowerCase}. To allow this, configure "allowedDevOrigins" in next.config\nRead more: https://nextjs.org/docs/app/api-reference/config/next-config-js/allowedDevOrigins`
- )
- return true
+ return warnOrBlockRequest(res, originLowerCase, mode)
}
}
}
diff --git a/test/development/basic/allowed-dev-origins.test.ts b/test/development/basic/allowed-dev-origins.test.ts
new file mode 100644
index 0000000000000..ee94826929328
--- /dev/null
+++ b/test/development/basic/allowed-dev-origins.test.ts
@@ -0,0 +1,277 @@
+import http from 'http'
+import { join } from 'path'
+import webdriver from 'next-webdriver'
+import { createNext, FileRef } from 'e2e-utils'
+import { NextInstance } from 'e2e-utils'
+import { fetchViaHTTP, findPort, retry } from 'next-test-utils'
+
+describe.each([['', '/docs']])(
+ 'allowed-dev-origins, basePath: %p',
+ (basePath: string) => {
+ let next: NextInstance
+
+ describe('warn mode', () => {
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ pages: new FileRef(join(__dirname, 'misc/pages')),
+ public: new FileRef(join(__dirname, 'misc/public')),
+ },
+ nextConfig: {
+ basePath,
+ },
+ })
+
+ await retry(async () => {
+ // make sure host server is running
+ const asset = await fetchViaHTTP(
+ next.appPort,
+ '/_next/static/chunks/pages/_app.js'
+ )
+ expect(asset.status).toBe(200)
+ })
+ })
+ afterAll(() => next.destroy())
+
+ it('should warn about WebSocket from cross-site', async () => {
+ let server = http.createServer((req, res) => {
+ res.end(`
+
+
+ testing cross-site
+
+
+
+ `)
+ })
+ try {
+ const port = await findPort()
+ await new Promise((res) => {
+ server.listen(port, () => res())
+ })
+ const websocketSnippet = `(() => {
+ const statusEl = document.createElement('p')
+ statusEl.id = 'status'
+ document.querySelector('body').appendChild(statusEl)
+
+ const ws = new WebSocket("${next.url}/_next/webpack-hmr")
+
+ ws.addEventListener('error', (err) => {
+ statusEl.innerText = 'error'
+ })
+ ws.addEventListener('open', () => {
+ statusEl.innerText = 'connected'
+ })
+ })()`
+
+ // ensure direct port with mismatching port is blocked
+ const browser = await webdriver(`http://127.0.0.1:${port}`, '/about')
+ await browser.eval(websocketSnippet)
+ await retry(async () => {
+ expect(await browser.elementByCss('#status').text()).toBe(
+ 'connected'
+ )
+ })
+
+ // ensure different host is blocked
+ await browser.get(`https://example.vercel.sh/`)
+ await browser.eval(websocketSnippet)
+ await retry(async () => {
+ expect(await browser.elementByCss('#status').text()).toBe(
+ 'connected'
+ )
+ })
+
+ expect(next.cliOutput).toContain('Cross origin request detected from')
+ } finally {
+ server.close()
+ }
+ })
+
+ it('should not allow loading scripts from cross-site', async () => {
+ let server = http.createServer((req, res) => {
+ res.end(`
+
+
+ testing cross-site
+
+
+
+ `)
+ })
+ try {
+ const port = await findPort()
+ await new Promise((res) => {
+ server.listen(port, () => res())
+ })
+ const scriptSnippet = `(() => {
+ const statusEl = document.createElement('p')
+ statusEl.id = 'status'
+ document.querySelector('body').appendChild(statusEl)
+
+ const script = document.createElement('script')
+ script.src = "${next.url}/_next/static/chunks/pages/_app.js"
+
+ script.onerror = (err) => {
+ statusEl.innerText = 'error'
+ }
+ script.onload = () => {
+ statusEl.innerText = 'connected'
+ }
+ document.querySelector('body').appendChild(script)
+ })()`
+
+ // ensure direct port with mismatching port is blocked
+ const browser = await webdriver(`http://127.0.0.1:${port}`, '/about')
+ await browser.eval(scriptSnippet)
+
+ await retry(async () => {
+ expect(await browser.elementByCss('#status').text()).toBe(
+ 'connected'
+ )
+ })
+
+ // ensure different host is blocked
+ await browser.get(`https://example.vercel.sh/`)
+ await browser.eval(scriptSnippet)
+
+ await retry(async () => {
+ expect(await browser.elementByCss('#status').text()).toBe(
+ 'connected'
+ )
+ })
+
+ expect(next.cliOutput).toContain('Cross origin request detected from')
+ } finally {
+ server.close()
+ }
+ })
+ })
+
+ describe('block mode', () => {
+ beforeAll(async () => {
+ next = await createNext({
+ files: {
+ pages: new FileRef(join(__dirname, 'misc/pages')),
+ public: new FileRef(join(__dirname, 'misc/public')),
+ },
+ nextConfig: {
+ basePath,
+ allowedDevOrigins: ['localhost'],
+ },
+ })
+
+ await retry(async () => {
+ // make sure host server is running
+ const asset = await fetchViaHTTP(
+ next.appPort,
+ '/_next/static/chunks/pages/_app.js'
+ )
+ expect(asset.status).toBe(200)
+ })
+ })
+ afterAll(() => next.destroy())
+
+ it('should not allow dev WebSocket from cross-site', async () => {
+ let server = http.createServer((req, res) => {
+ res.end(`
+
+
+ testing cross-site
+
+
+
+ `)
+ })
+ try {
+ const port = await findPort()
+ await new Promise((res) => {
+ server.listen(port, () => res())
+ })
+ const websocketSnippet = `(() => {
+ const statusEl = document.createElement('p')
+ statusEl.id = 'status'
+ document.querySelector('body').appendChild(statusEl)
+
+ const ws = new WebSocket("${next.url}/_next/webpack-hmr")
+
+ ws.addEventListener('error', (err) => {
+ statusEl.innerText = 'error'
+ })
+ ws.addEventListener('open', () => {
+ statusEl.innerText = 'connected'
+ })
+ })()`
+
+ // ensure direct port with mismatching port is blocked
+ const browser = await webdriver(`http://127.0.0.1:${port}`, '/about')
+ await browser.eval(websocketSnippet)
+ await retry(async () => {
+ expect(await browser.elementByCss('#status').text()).toBe('error')
+ })
+
+ // ensure different host is blocked
+ await browser.get(`https://example.vercel.sh/`)
+ await browser.eval(websocketSnippet)
+ await retry(async () => {
+ expect(await browser.elementByCss('#status').text()).toBe('error')
+ })
+ } finally {
+ server.close()
+ }
+ })
+
+ it('should not allow loading scripts from cross-site', async () => {
+ let server = http.createServer((req, res) => {
+ res.end(`
+
+
+ testing cross-site
+
+
+
+ `)
+ })
+ try {
+ const port = await findPort()
+ await new Promise((res) => {
+ server.listen(port, () => res())
+ })
+ const scriptSnippet = `(() => {
+ const statusEl = document.createElement('p')
+ statusEl.id = 'status'
+ document.querySelector('body').appendChild(statusEl)
+
+ const script = document.createElement('script')
+ script.src = "${next.url}/_next/static/chunks/pages/_app.js"
+
+ script.onerror = (err) => {
+ statusEl.innerText = 'error'
+ }
+ script.onload = () => {
+ statusEl.innerText = 'connected'
+ }
+ document.querySelector('body').appendChild(script)
+ })()`
+
+ // ensure direct port with mismatching port is blocked
+ const browser = await webdriver(`http://127.0.0.1:${port}`, '/about')
+ await browser.eval(scriptSnippet)
+ await retry(async () => {
+ expect(await browser.elementByCss('#status').text()).toBe('error')
+ })
+
+ // ensure different host is blocked
+ await browser.get(`https://example.vercel.sh/`)
+ await browser.eval(scriptSnippet)
+
+ await retry(async () => {
+ expect(await browser.elementByCss('#status').text()).toBe('error')
+ })
+ } finally {
+ server.close()
+ }
+ })
+ })
+ }
+)
diff --git a/test/development/basic/misc.test.ts b/test/development/basic/misc.test.ts
index 37a0773aa7375..ddb24527c11a4 100644
--- a/test/development/basic/misc.test.ts
+++ b/test/development/basic/misc.test.ts
@@ -1,10 +1,9 @@
import url from 'url'
-import http from 'http'
import { join } from 'path'
import webdriver from 'next-webdriver'
import { createNext, FileRef } from 'e2e-utils'
import { NextInstance } from 'e2e-utils'
-import { fetchViaHTTP, findPort, renderViaHTTP, retry } from 'next-test-utils'
+import { fetchViaHTTP, renderViaHTTP } from 'next-test-utils'
describe.each([[''], ['/docs']])(
'misc basic dev tests, basePath: %p',
@@ -43,107 +42,6 @@ describe.each([[''], ['/docs']])(
})
describe('With Security Related Issues', () => {
- it('should not allow dev WebSocket from cross-site', async () => {
- let server = http.createServer((req, res) => {
- res.end(`
-
-
- testing cross-site
-
-
-
- `)
- })
- try {
- const port = await findPort()
- await new Promise((res) => {
- server.listen(port, () => res())
- })
- const websocketSnippet = `(() => {
- const statusEl = document.createElement('p')
- statusEl.id = 'status'
- document.querySelector('body').appendChild(statusEl)
-
- const ws = new WebSocket("${next.url}/_next/webpack-hmr")
-
- ws.addEventListener('error', (err) => {
- statusEl.innerText = 'error'
- })
- ws.addEventListener('open', () => {
- statusEl.innerText = 'connected'
- })
- })()`
-
- // ensure direct port with mismatching port is blocked
- const browser = await webdriver(`http://127.0.0.1:${port}`, '/about')
- await browser.eval(websocketSnippet)
- await retry(async () => {
- expect(await browser.elementByCss('#status').text()).toBe('error')
- })
-
- // ensure different host is blocked
- await browser.get(`https://example.vercel.sh/`)
- await browser.eval(websocketSnippet)
- await retry(async () => {
- expect(await browser.elementByCss('#status').text()).toBe('error')
- })
- } finally {
- server.close()
- }
- })
-
- it('should not allow loading scripts from cross-site', async () => {
- let server = http.createServer((req, res) => {
- res.end(`
-
-
- testing cross-site
-
-
-
- `)
- })
- try {
- const port = await findPort()
- await new Promise((res) => {
- server.listen(port, () => res())
- })
- const scriptSnippet = `(() => {
- const statusEl = document.createElement('p')
- statusEl.id = 'status'
- document.querySelector('body').appendChild(statusEl)
-
- const script = document.createElement('script')
- script.src = "${next.url}/_next/static/chunks/pages/_app.js"
-
- script.onerror = (err) => {
- statusEl.innerText = 'error'
- }
- script.onload = () => {
- statusEl.innerText = 'connected'
- }
- document.querySelector('body').appendChild(script)
- })()`
-
- // ensure direct port with mismatching port is blocked
- const browser = await webdriver(`http://127.0.0.1:${port}`, '/about')
- await browser.eval(scriptSnippet)
- await retry(async () => {
- expect(await browser.elementByCss('#status').text()).toBe('error')
- })
-
- // ensure different host is blocked
- await browser.get(`https://example.vercel.sh/`)
- await browser.eval(scriptSnippet)
-
- await retry(async () => {
- expect(await browser.elementByCss('#status').text()).toBe('error')
- })
- } finally {
- server.close()
- }
- })
-
it('should not allow accessing files outside .next/static and .next/server directory', async () => {
const pathsToCheck = [
basePath + '/_next/static/../BUILD_ID',