Security context
Medium· 6.5GHSA-4342-x723-ch2f CVE-2025-57822CWE-918Published Aug 29, 2025

Next.js Improper Middleware Redirect Handling Leads to SSRF

Research this vulnerability

Research is free — Hunters explains how the bug works, the root-cause code pattern, how the fix addresses it, and how to test whether a target is affected, in chat. Investigate & write exploit is a paid run — the engine reads the advisory and fix commits, then builds and validates a working proof-of-concept exploit with reproduction steps.

Affected versions

0.9.9 → fixed in 14.2.3215.0.0-canary.0 → fixed in 15.4.7

Details

A vulnerability in **Next.js Middleware** has been fixed in **v14.2.32** and **v15.4.7**. The issue occurred when request headers were directly passed into `NextResponse.next()`. In self-hosted applications, this could allow Server-Side Request Forgery (SSRF) if certain sensitive headers from the incoming request were reflected back into the response. All users implementing custom middleware logic in self-hosted environments are strongly encouraged to upgrade and verify correct usage of the `next()` function. More details at [Vercel Changelog](https://vercel.com/changelog/cve-2025-57822)

The fix

fix router handling when setting a location response header

Zack Tanner· Aug 13, 2025, 12:26 AM+68109c9aaed5bb
packages/next/src/server/lib/router-utils/resolve-routes.ts+33 9
@@ -18,7 +18,10 @@ import { formatHostname } from '../format-hostname'
import { toNodeOutgoingHttpHeaders } from '../../web/utils'
import { isAbortError } from '../../pipe-readable'
import { getHostname } from '../../../shared/lib/get-hostname'
-import { getRedirectStatus } from '../../../lib/redirect-status'
+import {
+ getRedirectStatus,
+ allowedStatusCodes,
+} from '../../../lib/redirect-status'
import { normalizeRepeatedSlashes } from '../../../shared/lib/utils'
import { getRelativeURL } from '../../../shared/lib/router/utils/relativize-url'
import { addPathPrefix } from '../../../shared/lib/router/utils/add-path-prefix'
@@ -659,15 +662,36 @@ export function getResolveRoutes(
if (middlewareHeaders['location']) {
const value = middlewareHeaders['location'] as string
- const rel = getRelativeURL(value, initUrl)
- resHeaders['location'] = rel
- parsedUrl = url.parse(rel, true)
- return {
- parsedUrl,
- resHeaders,
- finished: true,
- statusCode: middlewareRes.status,
+ // Only process Location header as a redirect if it has a proper redirect status
+ // This prevents a Location header with non-redirect status from being treated as a redirect
+ const isRedirectStatus = allowedStatusCodes.has(
+ middlewareRes.status
+ )
+
+ if (isRedirectStatus) {
+ // Process as redirect: update parsedUrl and convert to relative URL
+ const rel = getRelativeURL(value, initUrl)
+ resHeaders['location'] = rel
+ parsedUrl = url.parse(rel, true)
+
+ return {
+ parsedUrl,
+ resHeaders,
+ finished: true,
+ statusCode: middlewareRes.status,
+ }
+ } else {
+ // Not a redirect: just pass through the Location header
+ resHeaders['location'] = value
+
+ return {
+ parsedUrl,
+ resHeaders,
+ finished: true,
+ bodyStream,
+ statusCode: middlewareRes.status,
+ }
}
}
test/e2e/app-dir/app-middleware/app-middleware.test.ts+24 1
@@ -6,7 +6,7 @@ import { nextTestSetup, FileRef } from 'e2e-utils'
import type { Response } from 'node-fetch'
describe('app-dir with middleware', () => {
- const { next } = nextTestSetup({
+ const { next, isNextDeploy } = nextTestSetup({
files: __dirname,
})
@@ -248,6 +248,29 @@ describe('app-dir with middleware', () => {
await browser.deleteCookies()
})
+
+ // TODO: This consistently 404s on Vercel deployments. It technically
+ // doesn't repro the bug we're trying to fix but we need to figure out
+ // why the handling is different.
+ if (!isNextDeploy) {
+ it('should not incorrectly treat a Location header as a rewrite', async () => {
+ const res = await next.fetch('/test-location-header')
+
+ // Should get status 200 (not a redirect status)
+ expect(res.status).toBe(200)
+
+ // Should get the JSON response associated with the route,
+ // and not follow the redirect
+ const json = await res.json()
+ expect(json).toEqual({ foo: 'bar' })
+
+ // Ensure the provided location is still on the response
+ const locationHeader = res.headers.get('location')
+ expect(locationHeader).toBe(
+ 'https://next-data-api-endpoint.vercel.app/api/random'
+ )
+ })
+ }
})
describe('app dir - middleware without pages dir', () => {
test/e2e/app-dir/app-middleware/middleware.js+11 0
@@ -78,6 +78,17 @@ export async function middleware(request) {
return res
}
+ if (request.nextUrl.pathname === '/test-location-header') {
+ return NextResponse.json(
+ { foo: 'bar' },
+ {
+ headers: {
+ location: 'https://next-data-api-endpoint.vercel.app/api/random',
+ },
+ }
+ )
+ }
+
return NextResponse.next({
request: {
headers: headersFromRequest,

References