Repository: vercel/next.js
Pull request: ensure /__next middleware URLs are included in the origin check

We have special development endpoints that are also prefixed under `/__nextjs`. This updates the origin checking logic to account for those in addition to `/_next`, and adds a test.

Changed files:
filename: packages/next/src/server/lib/router-utils/block-cross-site.ts
patch: @@ -50,8 +50,9 @@ export const blockCrossSite = (
     allowedOrigins.push(hostname)
   }
 
-  // only process _next URLs
-  if (!req.url?.includes('/_next')) {
+  // only process internal URLs/middleware
+  // TODO: We should standardize on a single prefix for this
+  if (!req.url?.includes('/_next') && !req.url?.includes('/__nextjs')) {
     return false
   }
   // block non-cors request from cross-site e.g. script tag on

filename: test/development/basic/allowed-dev-origins.test.ts
patch: @@ -5,6 +5,29 @@ import { createNext, FileRef } from 'e2e-utils'
 import { NextInstance } from 'e2e-utils'
 import { fetchViaHTTP, findPort, retry } from 'next-test-utils'
 
+async function createHostServer() {
+  const server = http.createServer((req, res) => {
+    res.end(`
+      <html>
+        <head>
+          <title>testing cross-site</title> 
+        </head>
+        <body></body>
+      </html>
+    `)
+  })
+
+  const port = await findPort()
+  await new Promise<void>((res) => {
+    server.listen(port, () => res())
+  })
+
+  return {
+    server,
+    port,
+  }
+}
+
 describe.each([['', '/docs']])(
   'allowed-dev-origins, basePath: %p',
   (basePath: string) => {
@@ -34,21 +57,8 @@ describe.each([['', '/docs']])(
       afterAll(() => next.destroy())
 
       it('should warn about WebSocket from cross-site', async () => {
-        let server = http.createServer((req, res) => {
-          res.end(`
-              <html>
-                <head>
-                  <title>testing cross-site</title>
-                </head>
-                <body></body>
-              </html>
-            `)
-        })
+        const { server, port } = await createHostServer()
         try {
-          const port = await findPort()
-          await new Promise<void>((res) => {
-            server.listen(port, () => res())
-          })
           const websocketSnippet = `(() => {
               const statusEl = document.createElement('p')
               statusEl.id = 'status'
@@ -88,22 +98,10 @@ describe.each([['', '/docs']])(
         }
       })
 
-      it('should not allow loading scripts from cross-site', async () => {
-        let server = http.createServer((req, res) => {
-          res.end(`
-              <html>
-                <head>
-                  <title>testing cross-site</title>
-                </head>
-                <body></body>
-              </html>
-            `)
-        })
+      it('should warn about loading scripts from cross-site', async () => {
+        const { server, port } = await createHostServer()
+
         try {
-          const port = await findPort()
-          await new Promise<void>((res) => {
-            server.listen(port, () => res())
-          })
           const scriptSnippet = `(() => {
               const statusEl = document.createElement('p')
               statusEl.id = 'status'
@@ -146,6 +144,46 @@ describe.each([['', '/docs']])(
           server.close()
         }
       })
+
+      it('should warn about loading internal middleware from cross-site', async () => {
+        const { server, port } = await createHostServer()
+        try {
+          const browser = await webdriver(`http://127.0.0.1:${port}`, '/about')
+
+          const middlewareSnippet = `(() => {
+            const statusEl = document.createElement('p')
+            statusEl.id = 'status'
+            document.querySelector('body').appendChild(statusEl)
+
+            const xhr = new XMLHttpRequest()
+            xhr.open('GET', '${next.url}/__nextjs_error_feedback?errorCode=0&wasHelpful=true', true)
+            xhr.send()
+
+            xhr.onload = () => {
+              statusEl.innerText = "OK"
+            }
+            xhr.onerror = () => {
+              statusEl.innerText = "Unauthorized"
+            }
+          })()`
+
+          await browser.eval(middlewareSnippet)
+
+          await retry(async () => {
+            // TODO: These requests seem to be blocked regardless of our handling only when running with Turbopack
+            // Investigate why this is the case
+            if (!process.env.TURBOPACK) {
+              expect(await browser.elementByCss('#status').text()).toBe('OK')
+            }
+
+            expect(next.cliOutput).toContain(
+              'Cross origin request detected from'
+            )
+          })
+        } finally {
+          server.close()
+        }
+      })
     })
 
     describe('block mode', () => {
@@ -173,21 +211,8 @@ describe.each([['', '/docs']])(
       afterAll(() => next.destroy())
 
       it('should not allow dev WebSocket from cross-site', async () => {
-        let server = http.createServer((req, res) => {
-          res.end(`
-              <html>
-                <head>
-                  <title>testing cross-site</title>
-                </head>
-                <body></body>
-              </html>
-            `)
-        })
+        const { server, port } = await createHostServer()
         try {
-          const port = await findPort()
-          await new Promise<void>((res) => {
-            server.listen(port, () => res())
-          })
           const websocketSnippet = `(() => {
               const statusEl = document.createElement('p')
               statusEl.id = 'status'
@@ -222,21 +247,8 @@ describe.each([['', '/docs']])(
       })
 
       it('should not allow loading scripts from cross-site', async () => {
-        let server = http.createServer((req, res) => {
-          res.end(`
-              <html>
-                <head>
-                  <title>testing cross-site</title>
-                </head>
-                <body></body>
-              </html>
-            `)
-        })
+        const { server, port } = await createHostServer()
         try {
-          const port = await findPort()
-          await new Promise<void>((res) => {
-            server.listen(port, () => res())
-          })
           const scriptSnippet = `(() => {
               const statusEl = document.createElement('p')
               statusEl.id = 'status'
@@ -272,6 +284,40 @@ describe.each([['', '/docs']])(
           server.close()
         }
       })
+
+      it('should not allow loading internal middleware from cross-site', async () => {
+        const { server, port } = await createHostServer()
+        try {
+          const browser = await webdriver(`http://127.0.0.1:${port}`, '/about')
+
+          const middlewareSnippet = `(() => {
+            const statusEl = document.createElement('p')
+            statusEl.id = 'status'
+            document.querySelector('body').appendChild(statusEl)
+
+            const xhr = new XMLHttpRequest()
+            xhr.open('GET', '${next.url}/__nextjs_error_feedback?errorCode=0&wasHelpful=true', true)
+            xhr.send()
+
+            xhr.onload = () => {
+              statusEl.innerText = "OK"
+            }
+            xhr.onerror = () => {
+              statusEl.innerText = "Unauthorized"
+            }
+          })()`
+
+          await browser.eval(middlewareSnippet)
+
+          await retry(async () => {
+            expect(await browser.elementByCss('#status').text()).toBe(
+              'Unauthorized'
+            )
+          })
+        } finally {
+          server.close()
+        }
+      })
     })
   }
 )