1. 漏洞初探:一个“内部人”的伪装术
最近在帮一个朋友排查他们公司Next.js应用的权限问题时,碰到了一个挺有意思的漏洞,官方编号是CVE-2025-29927。简单来说,这个漏洞能让攻击者用一个“内部通行证”,大摇大摆地绕过你精心设计的登录和权限检查,直接访问那些本该需要认证才能看的页面。这感觉就像有人伪造了员工工牌,骗过了大楼的门禁系统,直接进入了核心办公区。
这个漏洞影响面其实挺广的,因为它出在Next.js框架一个非常核心的机制——**中间件(Middleware)**上。很多开发者,包括我自己以前也喜欢用中间件来做统一的身份认证和权限校验,比如检查用户有没有登录,有没有访问某个页面的角色权限。把这块逻辑写在中间件里,代码干净又省事。但问题就出在,Next.js为了让中间件自己调用自己时不陷入死循环,设计了一个“内部请求”的标识机制。而这个机制,被外部攻击者巧妙地利用了。
想象一下这个场景:你的中间件就像公司的前台保安,每个访客(HTTP请求)进来,保安都要检查工牌(验证Token或Session)。但公司内部员工(Next.js框架自己发起的内部请求)进出频繁,每次都检查太麻烦,所以公司规定,内部员工可以佩戴一个特殊的“内部通行”袖标(x-middleware-subrequest请求头),保安看到这个袖标就直接放行,不再盘问。CVE-2025-29927这个漏洞的本质,就是攻击者不知怎么搞到了这个“内部通行”袖标,或者自己仿造了一个,然后戴着它大摇大摆地走进了公司,保安一看袖标,以为是同事,问都不问就让他进去了。
所以,这个漏洞的关键词就是 “中间件认证绕过” 。它不是一个让你能远程执行代码的“核弹级”漏洞,但它是一个典型的“逻辑漏洞”,破坏的是你应用最根本的访问控制规则。对于电商、后台管理、内容付费这类强权限依赖的应用来说,这无疑是致命的。攻击者可能无需窃取任何密码,就能直接查看其他用户的订单、操作管理员功能、下载付费内容。
2. 漏洞原理深挖:中间件为何“开了后门”?
要彻底理解这个漏洞,我们不能只停留在比喻上,得看看Next.js中间件到底是怎么工作的,以及那个“内部通行证”机制具体是怎么实现的。我翻看了Next.js的相关源码和官方文档,并结合自己的测试,把它的运行逻辑捋清楚了。
2.1 Next.js中间件的正常执勤流程
首先,我们得明白一个正常的、用于认证的Next.js中间件大概长什么样。下面是一个最简单的例子:
// middleware.js
import { NextResponse } from 'next/server';
import { verifyAuth } from './lib/auth'; // 假设的认证函数
export function middleware(request) {
// 1. 从请求中获取认证信息,比如Cookie里的token
const token = request.cookies.get('sessionToken')?.value;
// 2. 进行认证检查
const isAuthenticated = verifyAuth(token);
// 3. 如果未认证,重定向到登录页或返回401错误
if (!isAuthenticated) {
// 对于页面请求,可以重定向
if (request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
// 对于API请求,直接返回未授权
return new Response('Unauthorized', { status: 401 });
}
// 4. 认证通过,放行请求到目标页面或API
return NextResponse.next();
}
// 配置中间件生效的路由范围
export const config = {
matcher: ['/dashboard/:path*', '/api/protected/:path*'],
};
这个流程很清晰:请求来了 -> 中间件拦截 -> 检查登录状态 -> 通过则放行,不通过则拦截。这个“保安”工作得很称职。
2.2 “内部通行证”机制的由来与缺陷
那么,x-middleware-subrequest这个头是干嘛的呢?这源于Next.js框架内部的一个设计。有时候,一个中间件在处理请求时,可能需要去调用应用内的另一个路由(比如获取一些用户数据来做更复杂的权限判断)。如果这个内部调用再次触发同一个中间件,就会形成无限循环,中间件自己调自己,没完没了。
为了防止这种情况,Next.js在发起这类**内部子请求(Internal Subrequest)**时,会自动给请求加上一个特殊的HTTP头:x-middleware-subrequest。当中间件看到这个头时,就会知道:“哦,这是我自己人(框架内部)发起的请求,不用再走一遍认证检查了,直接放行去处理业务逻辑吧。”
这个设计的初衷是好的,是为了提升效率和避免死循环。但漏洞就出在:Next.js没有验证这个“内部通行证”是谁发的。
在漏洞版本中,中间件的逻辑简化后是这样的:
// 漏洞版本的内部逻辑(伪代码)
function handleRequest(req) {
// 检查请求头中是否存在 `x-middleware-subrequest`
if (req.headers.has('x-middleware-subrequest')) {
// 如果存在,则认为是内部请求,跳过所有中间件逻辑!
return directlyPassToRouteHandler(req);
}
// 不存在,则正常执行中间件函数(包括我们的认证检查)
return executeMiddleware(req);
}
看到问题了吗?这个检查只问“有没有通行证”,而不问“通行证是谁发的”。对于攻击者来说,这就简单得离谱了。他不需要破解任何加密算法,不需要伪造签名,他只需要在发给你的Next.js应用的任意一个HTTP请求里,手动加上一行请求头:
x-middleware-subrequest: 1
是的,值甚至可以是任意非空值,比如1、true,或者留空只写一个头字段名。中间件一看到这个头,立刻“立正敬礼”,认为这是来自框架内部的信任请求,二话不说就跳过了所有安全检查,把请求直接交给了后端的页面或API处理器。你写的所有verifyAuth、角色检查、权限逻辑,在这一刻全部形同虚设。
2.3 漏洞触发的核心条件与影响范围
这个漏洞的触发需要两个条件同时满足:
- 应用使用了Next.js中间件来实施关键的安全控制,如身份认证、权限校验。
- 应用的Next.js版本在受影响范围内。根据官方通告,受影响的版本主要包括:
- Next.js 15.x系列,版本号小于15.2.3
- Next.js 14.x系列,版本号小于14.2.25
- Next.js 13.5.6及以下版本(从11.1.4开始)
如果你的应用虽然用了中间件,但只是用它来做一些非安全相关的工作,比如日志记录、添加响应头,那这个漏洞的影响就有限。但现实是,由于中间件的便利性,用它来做第一道安全防线是非常普遍的做法。因此,这个漏洞的潜在影响范围相当大。攻击者利用此漏洞,可以:
- 直接访问需要登录才能查看的用户个人中心、订单列表。
- 直接调用需要管理员权限的API接口,进行数据增删改查。
- 访问静态生成(SSG)或服务器端渲染(SSR)的受保护页面,获取敏感信息。
3. 实战复现:亲手验证漏洞的存在
光讲原理可能还有点抽象,我带你实际动手搭一个环境,看看这个漏洞到底是怎么被利用的。这个过程能让你更直观地理解攻击者的视角。放心,我们只在本地安全的环境下进行。
3.1 搭建一个存在漏洞的演示环境
最快的方式是使用社区安全研究人员已经准备好的漏洞演示项目。我们用一个简单的例子来模拟:
首先,创建一个新的Next.js应用(使用受影响范围内的版本,这里我们用14.x的某个旧版本为例,请注意,生产环境请务必升级):
npx create-next-app@14.0.0 vulnerable-app
cd vulnerable-app
然后,我们创建并修改 middleware.js 文件,模拟一个简单的认证逻辑:
// middleware.js
import { NextResponse } from 'next/server';
export function middleware(request) {
// 模拟认证检查:检查cookie中是否有‘authenticated=true’
const isAuthenticated = request.cookies.get('authenticated')?.value === 'true';
// 保护 /dashboard 路径
if (request.nextUrl.pathname.startsWith('/dashboard')) {
console.log(`[Middleware] 访问 /dashboard,认证状态: ${isAuthenticated}`);
if (!isAuthenticated) {
// 未认证,重定向到首页
console.log('[Middleware] 认证失败,重定向到 /');
return NextResponse.redirect(new URL('/', request.url));
}
}
// 认证通过,放行
console.log('[Middleware] 认证通过或路径不受保护,放行');
return NextResponse.next();
}
export const config = {
matcher: '/dashboard/:path*',
};
接着,创建受保护的页面 app/dashboard/page.js:
// app/dashboard/page.js
export default function DashboardPage() {
return (
<div>
<h1>受保护的仪表板</h1>
<p>只有登录用户才能看到这个页面!</p>
</div>
);
}
最后,修改首页 app/page.js,添加一个简单的“登录”按钮(通过设置Cookie模拟登录):
// app/page.js
'use client';
import { useRouter } from 'next/navigation';
export default function Home() {
const router = useRouter();
const simulateLogin = () => {
document.cookie = "authenticated=true; path=/";
alert('已模拟登录!');
router.refresh();
};
return (
<div>
<h1>首页(公开)</h1>
<button onClick={simulateLogin}>点击模拟登录</button>
<p>登录后,你可以访问 <a href="/dashboard">/dashboard</a></p>
</div>
);
}
现在,启动你的开发服务器:
npm run dev
3.2 发起攻击:如何绕过认证
环境搭好了,我们来扮演攻击者。
-
正常访问测试:首先,在浏览器中打开
http://localhost:3000/dashboard。你会看到页面被自动重定向回了首页/,并且浏览器控制台或服务器日志中会显示[Middleware] 认证失败,重定向到 /。这说明我们的中间件保安在工作,挡住了未认证的请求。 -
利用漏洞绕过:接下来,我们使用一个工具来发送一个携带特殊请求头的请求。最简单的方法是使用 curl 命令(如果你没有,也可以用浏览器的开发者工具中的“网络”标签页,复制为cURL,然后修改)。打开你的终端,运行:
curl -H "x-middleware-subrequest: 1" http://localhost:3000/dashboard
看看返回了什么?你应该会直接看到 <h1>受保护的仪表板</h1> 这个页面的HTML内容!而你的服务器日志里,很可能没有打印出中间件里的认证失败日志,或者只显示了放行日志。
发生了什么?
curl命令发送的HTTP请求,手动加上了 x-middleware-subrequest: 1 这个头。Next.js的中间件(在漏洞版本中)看到这个头,错误地认为:“这是一个框架内部的子请求,是可信的,跳过所有检查。” 于是,请求直接被传递给了 app/dashboard/page.js,页面被渲染并返回给了我们。我们设置的Cookie认证检查完全被绕过了。
你还可以用浏览器插件(如ModHeader)或编写一段简单的Node.js脚本来发送这个恶意请求,效果是一样的。这就清晰地复现了攻击者是如何在不登录、不持有任何有效凭证的情况下,直接访问到受保护资源的。
4. 全面防御:从紧急止血到彻底修复
验证了漏洞的存在和危害,接下来肯定是想办法堵上这个洞。防御措施分几个层面,从立即可做的临时缓解,到一劳永逸的彻底修复,再到提升整体安全水位的最佳实践。
4.1 临时缓解措施(紧急止血)
如果你的线上业务正在使用受影响版本的Next.js,升级版本可能需要一些测试和部署时间,那么第一时间可以采取一个简单的临时方案,在中间件里主动拦截这个恶意头。
修改你的 middleware.js 文件,在中间件函数的最开头添加检查:
// middleware.js - 添加临时防护
import { NextResponse } from 'next/server';
import { verifyAuth } from './lib/auth';
export function middleware(request) {
// --- 临时修复:直接拒绝任何携带 x-middleware-subrequest 头的请求 ---
if (request.headers.has('x-middleware-subrequest')) {
// 记录日志,便于发现攻击尝试
console.warn(`[安全警报] 拦截携带 x-middleware-subrequest 头的可疑请求: ${request.method} ${request.nextUrl.pathname}`);
// 直接返回 403 Forbidden 或 400 Bad Request
return new Response('Forbidden: Suspicious request header detected.', {
status: 403,
});
}
// --- 临时修复结束 ---
// 原有的认证逻辑
const token = request.cookies.get('sessionToken')?.value;
const isAuthenticated = verifyAuth(token);
if (!isAuthenticated && request.nextUrl.pathname.startsWith('/dashboard')) {
return NextResponse.redirect(new URL('/login', request.url));
}
return NextResponse.next();
}
这个方法的原理是“一刀切”:既然框架无法正确识别这个头的真伪,那我们就不允许任何外部请求携带这个头。真正的Next.js内部子请求是在服务器内存中发起的,根本不会经过网络传输到达你这个中间件函数(在漏洞修复前,错误的逻辑让外部请求也能利用它)。所以,拦截所有携带此头的请求是安全的。
注意:这是一个临时方案。它虽然能有效阻断利用此漏洞的攻击,但理论上,如果未来Next.js的内部机制发生变化,或者你的应用有极其特殊的架构,可能会产生误伤。因此,它只是为你争取升级时间的权宜之计。
4.2 根本解决方案:升级Next.js版本
最彻底、最推荐的做法是立即将Next.js升级到已修复该漏洞的安全版本。Vercel官方在漏洞披露后迅速发布了补丁。
请根据你当前使用的Next.js主版本,升级到以下或更高版本:
- 如果你在使用 Next.js 15.x:请升级到 15.2.3 或更高版本。
- 如果你在使用 Next.js 14.x:请升级到 14.2.25 或更高版本。
- 如果你在使用 Next.js 13.x 或更早的受支持版本:请查阅官方安全公告,升级到对应的修复版本。对于已结束生命周期的版本(如11.x, 12.x),强烈建议升级到受支持的LTS版本。
升级命令通常很简单:
# 使用 npm
npm install next@latest
# 或使用 yarn
yarn upgrade next@latest
# 或指定确切的安全版本
npm install next@15.2.3
升级后务必进行充分的测试,因为主要版本升级(如从14到15)可能包含不兼容的变更。重点测试你的中间件逻辑、认证授权流程以及核心业务功能。
4.3 防御性编程与安全最佳实践
除了修复这个特定漏洞,我们更应该从这次事件中吸取教训,加强应用整体的安全防御能力。中间件是强大的,但单一依赖点也是风险的。
1. 实施纵深防御(Defense in Depth) 不要仅仅依赖中间件这一道安全门。在关键的API路由或页面组件中,进行第二次权限校验。
- 对于API路由(App Router):在
app/api/protected/route.js中,再次验证用户会话。// app/api/protected/route.js import { getServerSession } from 'next-auth'; // 示例使用 next-auth import { NextResponse } from 'next/server'; export async function GET(request) { const session = await getServerSession(); if (!session) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); } // ... 你的业务逻辑 } - 对于页面组件(Server Component):在服务端组件中检查并处理未授权状态。
// app/dashboard/page.js import { redirect } from 'next/navigation'; import { getCurrentUser } from '@/lib/auth'; export default async function DashboardPage() { const user = await getCurrentUser(); if (!user) { redirect('/login'); } return <div>欢迎回来,{user.name}</div>; }
这样,即使中间件被意外绕过(无论是由于这个漏洞还是其他未来可能出现的逻辑缺陷),关键资源仍然受到保护。
2. 审慎使用和测试中间件
- 明确中间件的匹配范围(matcher):使用
export const config精确控制中间件生效的路由,避免不必要的全局拦截,减少攻击面。 - 中间件逻辑保持简单和专注:尽量避免在中间件中实现过于复杂、容易出错的业务逻辑。它应该专注于路由守卫、请求/响应修改等横切关注点。
- 对中间件进行安全测试:将中间件纳入你的安全测试流程。可以尝试模拟各种畸形请求、特殊请求头,验证其行为是否符合预期。像CVE-2025-29927这种漏洞,通过简单的模糊测试(Fuzzing)就有可能被发现。
3. 建立请求头管理规范
- 清理传入的请求头:考虑在应用的最外层(如自定义服务器、边缘函数或专门的净化中间件)对传入的HTTP请求头进行过滤,移除或重命名非标准或内部使用的头(如
x-middleware-subrequest,x-forwarded-*等需谨慎处理)。 - 不要信任客户端传来的任何数据:这虽然是老生常谈,但永远是安全的第一原则。无论是请求头、Cookie、URL参数还是请求体,都必须经过严格的验证和清理。
5. 漏洞背后的思考:框架安全与开发者责任
CVE-2025-29927这个漏洞给我们敲响了一次警钟。它不仅仅是一个需要打补丁的技术问题,更引出了关于如何使用现代开发框架、如何理解框架抽象层背后逻辑的重要议题。
框架的“魔法”与透明度:像Next.js这样的高阶框架,提供了大量“开箱即用”的魔法功能,极大地提升了开发效率。中间件自动跳过内部子请求以避免循环,本是一个贴心的设计。但问题在于,这种内部机制对开发者可能不够透明。当框架自动添加、处理某些头或行为时,如果文档没有特别强调其安全含义,开发者很容易忽略其潜在风险。这就要求框架开发者在设计此类特性时,必须将安全性作为首要考量,并提供清晰的文档说明其边界和假设。
开发者的“黑盒”依赖风险:作为应用开发者,我们常常习惯于信任所选框架的安全性。但这次事件告诉我们,完全依赖框架提供的抽象是危险的。我们需要花时间去理解核心机制的工作原理,至少是那些涉及安全(路由、认证、渲染)的部分。知道中间件大概是怎么运行的,知道getServerSideProps和Server Component的执行时机差异,这些知识能在关键时刻帮你预判风险。
安全是持续的过程,不是一次性任务:修复CVE-2025-29927不是终点。订阅你所用框架和核心依赖库的安全公告(比如GitHub的Security Alerts,或官方博客),定期更新依赖,将安全扫描(如npm audit、yarn audit、Trivy等)集成到CI/CD流程中,这些都是现代软件开发必须养成的习惯。这次是x-middleware-subrequest,下次可能是别的什么头或参数。建立起主动发现和响应安全威胁的流程,远比每次漏洞出现后被动救火要有效得多。
在我自己的项目里,遇到这个漏洞后,我做的第一件事是升级版本,第二件事就是重新审视了所有中间件的逻辑,并在几个最关键的业务API里加上了第二道权限校验。多花一两个小时,换来的是夜里能睡得更安稳。框架让我们跑得更快,但看清脚下的路,系好安全带,始终是我们自己的责任。
原理与防御策略&spm=1001.2101.3001.5002&articleId=152058713&d=1&t=3&u=f48e2de5373f4711ad274209472ad6c0)
2470

被折叠的 条评论
为什么被折叠?



