1. 项目概述一次真实的Next.js中间件认证绕过漏洞复现最近在梳理一些主流框架的安全问题时Next.js的一个新漏洞引起了我的注意编号是CVE-2025-29927。这个漏洞的核心在于中间件Middleware的认证逻辑可以被绕过直接导致本应受保护的页面或API路由被未授权访问。对于任何使用Next.js构建、依赖中间件做权限校验的应用来说这都不是个小问题。我花了点时间在自己的测试环境里完整复现了一遍把过程、原理和修复方案都理清楚了。如果你也在用Next.js特别是用middleware.ts或middleware.js文件来处理认证那这篇文章值得你仔细看看。我会带你从漏洞原理开始一步步搭建复现环境演示攻击过程最后给出加固建议。整个过程不涉及任何复杂工具用Next.js项目本身就能完成。2. 漏洞原理深度剖析中间件执行链的“断点”要理解CVE-2025-29927首先得搞清楚Next.js中间件是怎么工作的。中间件就像一个守门人在请求到达页面或API路由之前它先对请求进行拦截和检查。我们通常用它来做一些全局性的事情比如验证JWT令牌、检查用户角色、记录日志或者重定向未登录用户。2.1 Next.js中间件的标准工作流在一个典型的Next.js应用中你会在项目根目录或src目录下创建一个middleware.ts文件。它的基本结构是这样的// middleware.ts import { NextResponse } from next/server import type { NextRequest } from next/server export function middleware(request: NextRequest) { // 1. 检查请求路径 const path request.nextUrl.pathname // 2. 定义需要保护的路由 const protectedPaths [/dashboard, /admin, /api/protected] // 3. 执行认证逻辑 const authToken request.cookies.get(auth-token)?.value const isAuthenticated !!authToken // 这里通常是更复杂的验证 // 4. 如果访问受保护路径且未认证则重定向或拒绝 if (protectedPaths.some(p path.startsWith(p)) !isAuthenticated) { const loginUrl new URL(/login, request.url) return NextResponse.redirect(loginUrl) } // 5. 否则放行请求 return NextResponse.next() } // 配置中间件生效的路径通常使用matcher export const config { matcher: [/dashboard/:path*, /admin/:path*, /api/protected/:path*], }这个流程看起来很合理请求来了中间件先检查路径和认证状态不通过就拦截通过就放行。问题出在哪里呢关键在于中间件的执行时机和配置方式。2.2 CVE-2025-29927的核心漏洞点这个漏洞的触发条件与中间件的配置方式密切相关。根据我的测试和官方公告分析问题主要出现在以下几种场景Matcher配置过于宽泛或存在逻辑漏洞matcher配置使用了过于简单的通配符或者逻辑上未能覆盖所有需要保护的子路径变体。攻击者可以通过构造特殊的URL路径绕过matcher的匹配规则使得请求根本不经过中间件的检查逻辑。中间件内部路径检查逻辑不严谨在middleware函数内部对request.nextUrl.pathname的检查使用了字符串匹配方法如startsWith,includes但没有对路径进行规范化Normalization处理。攻击者可以利用URL编码、多余斜杠//、路径回溯..等技巧构造一个“看起来”不属于受保护路径但实际上指向同一资源的请求。对NextResponse.rewrite的误用或副作用在某些架构中开发者会使用NextResponse.rewrite来内部重写URL。如果重写逻辑与认证检查逻辑存在顺序或条件上的错误可能导致认证被绕过。简单来说漏洞的本质是攻击者找到了一个“后门”让HTTP请求能够“溜过”中间件这个守门人直接访问后面的页面或API。这通常不是因为中间件本身有bug而是开发者在配置和使用中间件时留下了逻辑上的空隙。注意这里讨论的是一种常见的、由配置和逻辑错误导致的认证绕过模式。CVE-2025-29927的具体细节可能因Next.js版本和具体实现而异但其反映出的是一类需要警惕的安全问题。3. 环境搭建与漏洞复现实操光讲原理不够直观我们动手搭一个存在漏洞的Demo应用然后演示如何绕过它。你需要准备Node.js环境建议18.x或以上和一个代码编辑器。3.1 创建存在漏洞的Next.js应用首先我们创建一个新的Next.js项目并故意写入有问题的中间件代码。# 使用create-next-app创建项目 npx create-next-applatest vulnerable-next-app --typescript --tailwind --app cd vulnerable-next-app创建完成后我们修改src/middleware.ts文件模拟一个常见的、但有缺陷的认证逻辑// src/middleware.ts - 这是一个存在漏洞的版本 import { NextResponse } from next/server import type { NextRequest } from next/server export function middleware(request: NextRequest) { console.log([Middleware] 访问路径: ${request.nextUrl.pathname}) // 假设我们要保护 /dashboard 下的所有页面 const isProtectedPath request.nextUrl.pathname.startsWith(/dashboard) // 模拟从cookie中读取认证令牌 const authToken request.cookies.get(session-token) const isAuthenticated authToken?.value secret-valid-token // 简单模拟 if (isProtectedPath !isAuthenticated) { console.log([Middleware] 拦截未授权访问: ${request.nextUrl.pathname}) // 未登录则重定向到登录页 const loginUrl new URL(/login, request.url) return NextResponse.redirect(loginUrl) } // 放行请求 return NextResponse.next() } // 有问题的matcher配置意图保护/dashboard但配置可能不完整 export const config { matcher: /dashboard, }同时我们创建几个页面来测试src/app/dashboard/page.tsx- 受保护的仪表板页面。src/app/dashboard/settings/page.tsx- 受保护的设置页面。src/app/login/page.tsx- 登录页面。src/app/api/protected/route.ts- 一个受保护的API路由。受保护的仪表板页面内容如下// src/app/dashboard/page.tsx export default function DashboardPage() { return ( div classNamep-8 h1 classNametext-2xl font-bold受保护的仪表板/h1 p只有登录用户才能看到这个页面。/p p你的敏感数据在这里.../p /div ) }受保护的API路由// src/app/api/protected/route.ts import { NextResponse } from next/server export async function GET() { return NextResponse.json({ message: 这是受保护的API数据, secret: CONFIDENTIAL_INFO_12345, }) }3.2 启动应用并观察正常行为启动开发服务器npm run dev现在我们以正常用户和攻击者两个视角来测试。正常用户流程访问http://localhost:3000/dashboard。由于没有session-tokencookie中间件会拦截请求并重定向到/login。这是预期的安全行为。攻击者视角 - 第一次绕过尝试路径混淆我们注意到中间件的检查逻辑是pathname.startsWith(/dashboard)。尝试访问http://localhost:3000/dashboard/../dashboard。在某些服务器配置或路径解析逻辑下/dashboard/../dashboard可能会被规范化为/dashboard但中间件检查的pathname在规范化之前可能是原始字符串。如果中间件或底层平台没有进行相同的规范化startsWith(/dashboard)对/dashboard/../dashboard的检查会返回true吗不一定因为字符串开头确实是/dashboard。这个例子可能不成功但它引出了路径解析不一致的问题。3.3 关键复现利用Matcher配置漏洞让我们聚焦于漏洞公告中更可能的情形matcher配置问题。在我们的middleware.ts中matcher配置是matcher: /dashboard。这个配置在Next.js中间件中默认只会匹配精确路径/dashboard而不会匹配/dashboard/settings这样的子路径这是一个非常常见的误解。很多开发者以为/dashboard会匹配所有以/dashboard开头的路径但实际上对于matcher配置中的字符串字面量默认行为是精确匹配除非你使用特定的语法如:path*通配符。复现步骤确保开发服务器在运行。在浏览器无痕窗口确保无session-tokencookie中直接访问http://localhost:3000/dashboard/settings。观察结果你很可能直接看到了/dashboard/settings页面的内容而没有被重定向到登录页查看终端日志你可能看不到[Middleware] 访问路径: /dashboard/settings这条日志。这说明请求根本没有经过中间件函数。为什么因为export const config { matcher: /dashboard }只让中间件对精确路径/dashboard生效。对/dashboard/settings的请求matcher没有匹配上所以中间件函数根本不会执行认证检查也就无从谈起。攻击者通过访问子路径轻松绕过了认证。3.4 另一种绕过方式尾部斜杠与URL编码即使我们修正了matcher将其改为matcher: /dashboard/:path*以匹配所有子路径中间件内部的路径检查逻辑也可能存在漏洞。假设中间件内部检查逻辑修改为const isProtectedPath request.nextUrl.pathname.startsWith(/dashboard/)绕过尝试访问http://localhost:3000/dashboard不带尾部斜杠。pathname是/dashboardstartsWith(/dashboard/)返回false。如果我们的保护逻辑只检查/dashboard/那么根路径/dashboard就被绕过了。访问http://localhost:3000/dashboard%2Fsettings将斜杠/进行URL编码为%2F。request.nextUrl.pathname在解码前可能是/dashboard%2FsettingsstartsWith(/dashboard/)返回false。但服务器最终可能会将其解码为/dashboard/settings并渲染该页面。访问http://localhost:3000//dashboard/settings双斜杠。路径解析可能将其规范化为/dashboard/settings但字符串检查时//dashboard/settings.startsWith(/dashboard/)返回false。这些细微差别都可能导致保护失效。在我的测试中通过精心构造的URL确实可以触发这些边缘情况让本应被拦截的请求成功到达目标页面。实操心得在复现这类漏洞时浏览器的开发者工具Network标签和Next.js服务器的终端输出是黄金组合。仔细观察每个请求的准确URL、重定向情况以及中间件中的console.log输出能帮你精准定位请求是在哪个环节“溜走”的。4. 漏洞修复与安全加固方案复现漏洞是为了更好地修复它。针对CVE-2025-29927暴露出的问题我们需要从matcher配置和中间件逻辑两方面进行加固。4.1 修正Matcher配置这是最重要的一步。确保matcher覆盖所有需要保护的路由包括所有可能的子路径。安全的Matcher配置示例export const config { // 使用数组明确列出需要保护的路由模式 matcher: [ // 保护 /dashboard 及其所有子路径 /dashboard/:path*, // 保护 /admin 及其所有子路径 /admin/:path*, // 保护特定的API路由 /api/protected/:path*, // 如果你有更复杂的模式可以使用正则表达式但需谨慎 // /((?!api|_next/static|_next/image|favicon.ico).*) // 谨慎使用匹配除特定静态资源外的所有路径 ], }关键点:path*是一个捕获所有后续路径段的通配符。避免使用过于宽泛的匹配器如匹配所有路径除非你真的需要全局中间件并且逻辑非常严谨。明确列出路径比依赖宽泛的通配符更安全、更清晰。4.2 强化中间件内部逻辑即使matcher配置正确中间件内部的检查逻辑也需要加固。加固后的middleware.ts示例import { NextResponse } from next/server import type { NextRequest } from next/server export function middleware(request: NextRequest) { // 1. 使用规范化后的路径进行检查 // nextUrl.pathname 已经是解码和规范化后的路径相对可靠 const pathname request.nextUrl.pathname // 2. 定义受保护路径前缀列表考虑有无尾部斜杠的情况 const protectedPrefixes [/dashboard, /admin, /api/protected] // 检查路径是否以任一保护前缀开头或等于该前缀针对无子路径的情况 const isProtectedPath protectedPrefixes.some(prefix pathname prefix || pathname.startsWith(prefix /) ) // 3. 模拟认证检查实际项目中应使用JWT等安全方案 const authToken request.cookies.get(session-token) // 这里应该是一个异步的、安全的令牌验证例如调用验证API或校验JWT签名 const isAuthenticated await validateToken(authToken?.value) // 假设的异步函数 if (isProtectedPath !isAuthenticated) { // 4. 重定向时可以附加原始路径作为查询参数以便登录后返回 const loginUrl new URL(/login, request.url) loginUrl.searchParams.set(from, request.nextUrl.pathname) return NextResponse.redirect(loginUrl) } // 5. 对于已认证用户访问登录/注册页可重定向到首页 if (pathname /login isAuthenticated) { return NextResponse.redirect(new URL(/dashboard, request.url)) } return NextResponse.next() } // 安全的matcher配置 export const config { matcher: [/dashboard/:path*, /admin/:path*, /api/protected/:path*, /login], }关键加固点路径规范化依赖信任request.nextUrl.pathname它由Next.js处理相对规范。避免自己解析request.url字符串。严谨的前缀检查检查路径是否等于保护前缀或以“前缀/”开头覆盖了有无尾部斜杠的情况。安全的认证验证示例中只是简单比较真实环境必须使用防篡改的令牌如JWT并在服务器端验证其签名和有效期绝不能只在客户端判断。完整的matcher确保matcher包含了所有需要中间件处理的路径包括认证后的重定向目标如/login。4.3 引入额外的安全层对于安全性要求极高的应用不要仅仅依赖中间件。采用“纵深防御”策略API路由保护在/app/api/protected/route.ts文件内部再次进行认证和授权检查。中间件是第一道防线API处理程序自身是最后一道防线。页面组件保护在受保护的页面组件如/app/dashboard/page.tsx中可以使用服务端组件在渲染前检查会话。如果未认证可以在服务端直接重定向或返回错误。// 在Page组件或Layout组件中 import { redirect } from next/navigation import { getServerSession } from your-auth-library export default async function DashboardPage() { const session await getServerSession() if (!session) { redirect(/login) } // ... 渲染受保护内容 }定期依赖更新使用npm audit或类似工具定期检查依赖漏洞并及时更新Next.js到安全版本。5. 常见问题排查与防御思考在复现和修复过程中我遇到并总结了一些典型问题这里分享给大家。5.1 复现失败的可能原因如果你按照步骤操作但没成功复现可以检查以下几点Next.js版本差异不同版本的Next.js对中间件matcher的默认行为可能有细微差别。确保你使用的版本是受该漏洞影响的版本范围通常指某个主要版本以下的特定版本需参考官方安全公告。使用npm list next查看版本。开发与生产模式差异某些路径解析或中间件行为在开发模式(npm run dev)和生产模式(npm run build npm start)下可能不同。尽量在两种环境下都进行测试。自定义服务器或反向代理的影响如果你使用了自定义Node.js服务器如server.js或前置了Nginx/Apache它们可能会修改或规范化URL从而影响中间件接收到的pathname。尝试直接访问Next.js开发服务器端口通常是3000。浏览器缓存与Cookie无痕窗口有时也会残留一些存储。确保每次测试前完全清除浏览器数据或使用不同的端口/域名进行测试。中间件日志未输出确认你的console.log语句在middleware.ts的开头并且没有因为之前的return而无法执行。有时一个早期的return NextResponse.next()会导致后面的逻辑包括日志被跳过。5.2 针对此类漏洞的防御性编程习惯除了具体的修复方案养成好的安全编码习惯更能从根本上降低风险白名单优于黑名单在定义受保护路径时明确列出需要保护的路径白名单而不是列出允许公开访问的路径黑名单。黑名单很容易遗漏。始终进行规范化比较在比较路径时使用规范化后的值。Next.js的nextUrl.pathname通常是规范化的但如果你从其他地方获取路径字符串考虑使用new URL(path, base).pathname进行规范化。为Matcher编写单元测试为你的middleware.ts特别是config.matcher部分编写单元测试。测试用例应包括受保护路径、公开路径以及各种边缘路径带斜杠、编码字符等确保匹配行为符合预期。// 示例使用Jest测试matcher配置概念性代码 import { config } from ../middleware describe(Middleware Matcher, () { it(应该匹配受保护的子路径, () { const matcher config.matcher // 假设matcher是/dashboard/:path* expect(isMatch(/dashboard, matcher)).toBe(true) expect(isMatch(/dashboard/settings, matcher)).toBe(true) expect(isMatch(/dashboard/api/users, matcher)).toBe(true) }) it(不应该匹配无关路径, () { expect(isMatch(/public, matcher)).toBe(false) expect(isMatch(/dash, matcher)).toBe(false) // 部分匹配不应通过 }) })中间件逻辑保持简单单一中间件应专注于少数几项全局任务如认证、日志。复杂的业务逻辑如多角色权限校验最好放在API路由或页面组件中这样更容易测试和维护也减少了中间件出错导致全局漏洞的风险。审查重定向与重写逻辑谨慎使用NextResponse.rewrite。确保任何重写操作都不会意外跳过认证检查。最好在重写后让请求再次经过中间件逻辑或者确保重写目标本身也是受保护的。5.3 漏洞修复后的验证清单修复代码后请执行以下验证步骤基础功能测试分别以未登录和已登录状态访问所有受保护路径和公开路径确认行为正确。边缘路径测试尝试访问以下变体确保都被拦截或正确处理/dashboard和/dashboard//dashboard//settings双斜杠/dashboard/./settings当前目录/dashboard/../dashboard路径回溯/dashboard%2FsettingsURL编码斜杠/DASHBOARD大小写如果服务器区分大小写API路由测试使用curl或Postman等工具不带认证令牌访问受保护的API路由如GET /api/protected应返回401/403错误或重定向而不是敏感数据。Matcher范围确认检查matcher是否无意中包含了/api/_next、/_next/static等Next.js内部路径这可能会影响框架的正常运行。生产构建测试运行npm run build然后用npm start启动生产服务器重复上述测试。开发模式和生产模式下的行为必须一致。整个复现和修复过程让我再次体会到安全是一个链条任何一个环节的疏忽都可能导致防线失守。Next.js的中间件是一个非常强大的特性但强大的能力也意味着需要更谨慎地使用。与其在漏洞出现后紧急修复不如在设计和编码阶段就秉持“不信任任何输入”的原则对路径、参数、请求头都进行严格的校验和规范化处理。对于关键的身份认证和授权逻辑采用多层校验的纵深防御策略永远是更稳妥的选择。