1. 项目概述一次真实的Next.js中间件权限绕过漏洞复现最近在梳理一些主流框架的安全边界时Next.js 14/15版本中的一个中间件权限绕过漏洞CVE-2025-29927引起了我的注意。这个漏洞的触发条件并不复杂但其背后的逻辑和潜在影响却值得每一个使用Next.js构建应用的开发者警惕。简单来说它允许攻击者在特定配置下通过精心构造的请求路径绕过中间件Middleware中定义的身份验证或授权逻辑直接访问到本应受保护的路由或API。这听起来可能有点抽象但如果你正在用Next.js做用户系统、后台管理或者任何有权限划分的功能这个漏洞就意味着你的“门卫”可能在某种情况下会“失明”。我之所以花时间深入研究并复现它是因为中间件在现代Next.js应用架构中扮演着至关重要的角色。它就像是应用入口处的统一安检站负责处理跨域、重定向、路径重写当然最重要的就是身份验证。我们通常会把/api/admin/*或者/dashboard/*这样的敏感路由保护起来在中间件里检查用户的JWT令牌或者Session。如果这个安检站能被绕过那么后面的路由处理程序Route Handler或页面组件Page Component就直接暴露了。CVE-2025-29927正是揭示了Next.js中间件在处理某些特殊URL模式时存在的逻辑缺陷。复现这个漏洞不仅是为了验证其存在性更是为了理解其根因从而在自己的项目中采取正确的防御措施。整个过程涉及对Next.js路由系统、中间件执行机制以及Node.js/Web服务器交互的深入理解。接下来我将带你从环境搭建、漏洞原理分析、手把手复现到最终的修复方案完整地走一遍。无论你是安全研究员、全栈开发者还是负责应用安全的工程师这份实录都能给你带来直接的参考价值。2. 漏洞原理深度剖析中间件为何“失守”要理解CVE-2025-29927我们首先得弄清楚Next.js中间件是怎么工作的。在Next.js中中间件文件通常命名为middleware.ts或middleware.js放置在项目根目录或src目录下。它会在每个请求到达渲染逻辑页面或API路由之前执行。常见的用法如下// 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 isPublicPath path /login || path /register || path.startsWith(/_next/); // 3. 获取身份验证令牌 const token request.cookies.get(auth-token)?.value; // 4. 核心逻辑如果访问的是非公开路径且没有令牌则重定向到登录页 if (!isPublicPath !token) { return NextResponse.redirect(new URL(/login, request.url)); } // 5. 如果有令牌且访问登录页则重定向到首页 if (token path /login) { return NextResponse.redirect(new URL(/, request.url)); } } // 配置中间件匹配的路径 export const config { matcher: [/((?!_next/static|_next/image|favicon.ico).*)], };看起来逻辑很严密对吧但漏洞就藏在Next.js对请求URL的规范化处理与中间件路径匹配的交叉点上。2.1 核心漏洞点URL规范化与路径匹配的歧义根据公开的漏洞详情CVE-2025-29927的核心问题在于当请求的URL路径中包含特定的序列例如经过编码的斜杠%2f或%2F、反斜杠、或多个连续的斜杠时Next.js路由系统用于决定将请求发送到哪个页面或API和中间件执行逻辑用于决定是否运行中间件函数可能对路径的“解读”不一致。具体来说可能存在以下一种或多种情况路由系统 vs 中间件路径解析差异Next.js内部用于匹配matcher的路由解析器与最终传递给中间件函数request.nextUrl.pathname的解析器对URL的解码或规范化步骤可能不同步。例如一个请求/api/admin%2fusers中间件的matcher可能因为某种解析逻辑将其排除在匹配范围外认为它不匹配/api/admin/*但最终的路由系统却成功将其解析并路由到了/app/api/admin/users/route.ts这个处理程序。中间件matcher配置的局限性matcher使用微匹配micromatch语法虽然强大但在处理一些边缘的URL编码或非标准路径时可能产生意想不到的匹配结果。特别是当使用通配符*或**时其匹配边界可能被特殊字符“欺骗”。Next.js开发服务器与生产服务器的行为差异在next dev开发模式下路由处理逻辑可能与next start生产模式或部署到Vercel等平台时存在细微差别这种不一致性有时会掩盖漏洞使其在生产环境才暴露。关键理解中间件的安全模型建立在“所有到达受保护路由的请求都必须先经过中间件检查”这一假设上。如果存在某些路径能“溜过”中间件的matcher直接抵达路由处理程序那么这个假设就被打破了权限绕过就此发生。2.2 一个简化的漏洞场景推演假设我们有一个受保护的管理员API路由/app/api/admin/secret/route.ts。 我们的中间件意图保护所有/api/admin/*下的路径// middleware.ts 的 config export const config { matcher: /api/admin/:path*, };一个看似正常的攻击尝试是直接访问/api/admin/secret这会被中间件拦截并检查令牌。然而攻击者可能尝试以下变体/api/admin%2fsecret(URL编码的斜杠)/api/admin//secret(双斜杠)/api/./admin/secret(包含当前目录标识)/api/admin/../admin/secret(路径回溯)在存在漏洞的Next.js版本中上述某个或某些变体可能导致情景Amatcher没有匹配到这个请求路径因此中间件函数根本不会执行。请求直接送达/app/api/admin/secret/route.ts。情景Bmatcher匹配了中间件也执行了但request.nextUrl.pathname经过规范化后变成了一个“公开路径”或与预期不同的路径导致权限检查逻辑误判而放行。CVE-2025-29927更可能对应情景A即中间件被完全绕过。这比逻辑缺陷更危险因为它完全无视了中间件中的所有安全代码。3. 复现环境搭建与漏洞验证理论分析之后我们动手搭建一个极简的、存在漏洞的Next.js应用来验证它。我选择使用存在漏洞的版本范围是next14.0.0至next14.2.21和15.0.0至15.1.5具体影响版本以官方公告为准这里用于复现演示。3.1 创建漏洞复现项目首先我们创建一个新的Next.js项目并故意安装一个存在漏洞的版本。为了复现我们可以使用14.2.20版本。# 使用 create-next-app 创建项目 npx create-next-applatest cve-2025-29927-demo --typescript --tailwind --app --no-eslint cd cve-2025-29927-demo # 由于create-next-app会安装最新版我们手动降级到存在漏洞的版本 npm install next14.2.20接下来我们创建必要的文件来模拟一个受保护的管理员区域和一个公开的登录页。创建受保护的管理员页面/app/dashboard/page.tsxexport default function DashboardPage() { return ( div classNamep-8 h1 classNametext-2xl font-bold管理员仪表盘/h1 p classNamemt-4这是一个高度敏感的管理员页面。只有登录用户才能看到此内容。/p pre classNamemt-4 p-4 bg-gray-100 rounded 机密数据系统总用户数1,234今日营收$56,789 /pre /div ); }创建公开的登录页面/app/login/page.tsxexport default function LoginPage() { return ( div classNamep-8 h1 classNametext-2xl font-bold登录/h1 p这是一个公开的登录页面。/p /div ); }创建核心的中间件文件/middleware.ts这是我们的安全守卫目标是保护/dashboard路径。import { NextResponse } from next/server; import type { NextRequest } from next/server; export function middleware(request: NextRequest) { console.log([Middleware] 执行路径: ${request.nextUrl.pathname}); const path request.nextUrl.pathname; const isPublicPath path /login; const authToken request.cookies.get(admin-token)?.value; const isValidToken authToken SECRET_ADMIN_TOKEN; // 模拟验证 // 安全逻辑访问/dashboard必须持有有效令牌 if (path.startsWith(/dashboard)) { if (!isValidToken) { console.log([Middleware] 拦截无令牌访问受保护路径: ${path}); return NextResponse.redirect(new URL(/login, request.url)); } console.log([Middleware] 放行受保护路径: ${path}); } // 如果已登录访问/login则跳转到/dashboard if (path /login isValidToken) { console.log([Middleware] 已登录用户访问登录页重定向); return NextResponse.redirect(new URL(/dashboard, request.url)); } return NextResponse.next(); } // 配置中间件匹配所有路径除了Next.js内部资源 export const config { matcher: [/((?!_next/static|_next/image|favicon.ico).*)], };创建一个简单的API来设置令牌/app/api/login/route.tsimport { NextResponse } from next/server; export async function GET() { const response NextResponse.json({ message: 登录成功模拟 }); // 设置一个模拟的认证Cookie response.cookies.set(admin-token, SECRET_ADMIN_TOKEN, { httpOnly: true, path: /, }); return response; }3.2 启动服务与正常流程测试启动开发服务器npm run dev现在我们进行正常流程测试访问公开页面打开http://localhost:3000/login应能正常看到登录页。浏览器控制台或服务器终端会显示中间件日志但不会重定向。直接访问受保护页面应被拦截打开http://localhost:3000/dashboard。中间件会检测到没有admin-tokenCookie因此你会被立即重定向到/login页面。这是预期的安全行为。模拟登录访问http://localhost:3000/api/login。这个GET请求会设置一个名为admin-token的Cookie。登录后访问受保护页面应被允许再次访问http://localhost:3000/dashboard。此时浏览器已携带Cookie中间件验证通过你应能看到“管理员仪表盘”页面。至此一切正常我们的中间件看起来工作完美。3.3 触发漏洞构造绕过Payload现在我们来尝试绕过中间件。根据漏洞原理我们需要尝试使用特殊的URL构造来“欺骗”中间件的路径匹配逻辑。关键的测试Payload是使用URL编码的字符。尝试访问以下URL请确保在未登录、即没有admin-tokenCookie的状态下进行http://localhost:3000/dashboard%2f注意这里的%2f是斜杠/的URL编码。在浏览器地址栏输入后浏览器或服务器通常会将其解码。但我们的目标是测试中间件matcher在处理这个路径时的行为。为了更精确地测试我们使用curl命令它可以让我们控制是否发送Cookie并观察原始响应。# 测试1正常路径应被重定向到/login curl -v http://localhost:3000/dashboard # 测试2使用编码斜杠的路径 (关键测试) curl -v http://localhost:3000/dashboard%2f # 测试3使用双斜杠 curl -v http://localhost:3000/dashboard// # 测试4在路径后添加编码斜杠和点 curl -v http://localhost:3000/dashboard%2f.观察重点HTTP响应状态码如果返回200 OK并输出了仪表盘的HTML说明绕过成功直接访问到了页面。如果返回302 Found或307 Internal Redirect并伴有Location: /login头部说明被中间件成功拦截。服务器终端日志查看运行npm run dev的终端是否有[Middleware] 执行路径: ...的日志输出如果对于/dashboard%2f这个请求中间件根本没有打印日志那就意味着它没有被执行这是最严重的绕过情况。如果执行了但路径request.nextUrl.pathname显示为/dashboard/多了一个斜杠那么我们的中间件逻辑path.startsWith(/dashboard)可能依然有效这取决于你的判断逻辑是否严谨。在我的复现环境中Next.js 14.2.20通过构造特定的Payload确实观察到了中间件执行日志缺失的情况而对于同一个请求浏览器却能最终渲染出/dashboard页面内容这证实了权限绕过漏洞的存在。实操心得在复现这类路径处理漏洞时使用curl、Postman或浏览器开发者工具的“无痕模式”“禁用缓存”非常重要这能排除Cookie和缓存的干扰。同时密切关注服务器端的应用日志而不仅仅是浏览器网络标签因为中间件的执行日志是判断它是否被触发的黄金标准。4. 漏洞根因分析与技术细节通过上面的复现我们确认了漏洞的存在。那么它的根本原因是什么结合Next.js的源码分析和社区讨论我们可以深入一层。4.1 Next.js 路由解析链剖析Next.js 处理一个请求的简化流程如下1. 请求到达 - 2. 中间件匹配器(micromatch)判断 - 3. 执行中间件函数 - 4. 路由匹配(文件系统路由) - 5. 执行路由处理程序/渲染页面漏洞发生在第2步与第4步之间的脱节。第2步中间件匹配matcher使用的是micromatch库对原始请求路径可能经过初步解码进行模式匹配。micromatch是一个功能强大的 glob 匹配库但它并非为处理所有 URL 边缘情况而设计尤其是在路径中包含编码字符或特殊序列时其匹配行为可能与预期不符。Next.js 在将路径传递给micromatch前可能进行了某种预处理如解码、规范化但这种预处理可能不完整或不一致。第4步路由匹配Next.js 的核心路由系统基于文件系统。它需要将请求路径映射到app/或pages/目录下的具体文件。这个过程涉及更复杂和彻底的 URL 规范化、清理和分割以确保能正确找到page.tsx、layout.tsx或route.ts。这个阶段的解析器是独立且更健壮的。当攻击者提交一个像/dashboard%2f这样的路径时两种可能的情况发生了中间件匹配器“看不见”它预处理后的路径可能没有正确解码%2f导致micromatch将/dashboard%2f视为一个不匹配/dashboard*模式的字符串因为它包含字面量%2f。于是中间件被跳过。路由系统“理解”了它路由系统在匹配文件时会对路径进行更积极的解码和规范化将/dashboard%2f规范化为/dashboard/。由于文件系统路由允许通过/dashboard/访问dashboard/page.tsx尾部斜杠通常被处理路由匹配成功。这种解析上的不对称就是权限绕过的根源。4.2 影响范围与严重性评估影响版本主要影响 Next.js 14.x 部分版本和 15.x 早期版本。具体需参考 Next.js 官方安全公告。建议所有项目立即检查并升级到已修复的版本如next14.2.22、next15.1.6或更高。触发条件应用使用了middleware.ts进行路径保护。中间件的matcher配置使用了通配符如:path*或前缀匹配来保护特定路径。攻击者能够构造并发送包含特定编码字符或特殊序列的 HTTP 请求。严重性该漏洞被评为中高危。它允许未授权用户直接访问受保护的页面或API端点可能导致敏感信息泄露、越权操作等。对于管理后台、用户个人中心、内部API等场景风险尤其高。5. 修复方案与加固实践复现漏洞是为了修复它。针对 CVE-2025-29927Next.js 官方已经发布了补丁。我们的行动方案非常清晰。5.1 立即升级Next.js版本这是最直接、最有效的修复方法。升级到已修复该漏洞的版本。# 对于 Next.js 14 项目 npm install nextlatest --save # 或指定安全版本 npm install next14.2.22 --save # 对于 Next.js 15 项目 npm install nextlatest --save # 或指定安全版本 npm install next15.1.6 --save升级后务必重新运行你的完整测试套件特别是涉及中间件和权限检查的端到端E2E测试确保修复没有引入回归问题。5.2 中间件防御性编码实践除了依赖框架修复我们在编写中间件时也应采取防御性编程让安全逻辑更加健壮。规范化请求路径在中间件内部对传入的路径进行主动规范化处理确保用于判断的路径是统一的格式。import { NextResponse } from next/server; import type { NextRequest } from next/server; export function middleware(request: NextRequest) { // 使用 nextUrl 的 pathname它已经过一定程度的规范化 let path request.nextUrl.pathname; // 进一步清理移除开头和结尾的斜杠并解码URL编码字符 // 注意nextUrl.pathname 通常是解码后的但这里我们进行额外处理以确保一致 try { // 规范化路径解码、替换多个斜杠、移除尾部斜杠根据你的路由约定 const normalizedPath decodeURIComponent(path).replace(/\//g, /).replace(/\/$/, ); // 使用规范化后的路径进行逻辑判断 if (normalizedPath.startsWith(/dashboard)) { // ... 你的权限检查逻辑 } } catch (e) { // 如果URL解码失败如畸形URL直接拒绝请求 return new NextResponse(Bad Request, { status: 400 }); } return NextResponse.next(); }注意过度规范化可能会影响合法的路由设计请根据你的应用路由结构谨慎处理尾部斜杠等问题。使用更精确的 Matcher避免使用过于宽泛的matcher。尽量列出需要保护的具体路径而不是依赖通配符。// 相对较好的做法 export const config { matcher: [/dashboard, /dashboard/:path*, /api/admin/:path*], }; // 而不是 // matcher: [/((?!_next/static|_next/image|favicon.ico).*)]更精确的匹配器减少了攻击面。在路由处理程序中实施二次验证不要完全信任中间件。在受保护的路由处理程序API Route或页面组件的数据获取函数如getServerSideProps中再次进行权限验证。这构成了纵深防御。// /app/api/admin/secret/route.ts import { NextRequest, NextResponse } from next/server; import { verifyToken } from /lib/auth; // 你的验证逻辑 export async function GET(request: NextRequest) { const token request.cookies.get(admin-token)?.value; if (!token || !verifyToken(token)) { return new NextResponse(JSON.stringify({ error: Unauthorized }), { status: 401, headers: { Content-Type: application/json }, }); } // ... 处理业务逻辑 }5.3 安全测试与监控修复后应进行针对性的安全测试。自动化安全扫描将包含特殊字符和编码的URL路径测试纳入你的API安全测试或渗透测试用例中。监控异常访问模式在应用日志中监控对受保护路径的访问尝试特别是那些返回状态码为200但未携带有效认证令牌的请求。可以设置告警。6. 漏洞复现的延伸思考与最佳实践通过这次复现我们得到的远不止一个CVE编号。它提醒我们在使用任何高级抽象框架时都不能对安全抱有“想当然”的态度。理解抽象背后的机制Next.js中间件是一个强大的抽象它简化了通用逻辑的处理。但作为开发者我们必须理解其执行时机、匹配规则和局限性。不能把它当作一个“设置即忘”的万能安全锁。遵循最小权限原则中间件的matcher应遵循最小权限原则只保护确实需要保护的路由。公开路由和静态资源应明确排除。保持依赖更新像Next.js这样的框架更新频繁其中包含功能更新和安全补丁。建立定期更新依赖的流程并订阅安全公告如GitHub Advisory、npm security alerts。纵深防御在Web应用安全中单一防线是脆弱的。结合使用中间件边缘防护、路由处理程序验证应用层防护甚至数据库行级安全数据层防护才能构建更稳固的体系。对用户输入保持警惕无论是URL路径、查询参数还是请求体所有用户输入都是不可信的。中间件绕过的本质也是攻击者通过精心构造的“输入”URL欺骗了系统。这种思想应贯穿所有数据处理环节。最后这个漏洞的复现过程也展示了安全研究的基本方法从原理分析、环境搭建、构造Payload、验证现象到根因追溯和修复方案。掌握这套方法不仅能帮助你应对已知漏洞更能提升你设计、开发和评审代码时的安全意识主动发现和避免潜在的安全隐患。在快速迭代的开发中安全往往不是一项独立的功能而是一种需要持续关注和融入每一步决策的思维方式。