CVE-2026-27971 Qwik 框架反序列化远程代码执行漏洞教学文档
漏洞概述
漏洞编号: CVE-2026-27971
影响产品: Qwik JavaScript 框架
受影响版本: ≤ 1.19.0
漏洞类型: 反序列化漏洞 → 远程代码执行
CVSS评分: 9.2 (高危)
披露时间: 2026年3月24日
修复版本: 1.19.1
漏洞描述
Qwik 是一个注重性能的 JavaScript 框架。在版本 1.19.0 及之前的 server$ 远程过程调用机制中,存在不安全的反序列化实现,使得任何未经验证的攻击者都能通过发送单个精心构造的 HTTP 请求,在服务器上执行任意系统命令,实现远程代码执行。该漏洞影响所有在 Node.js 运行时上部署的、使用了受影响版本 Qwik 框架的应用。
漏洞核心原理
漏洞的根本原因在于,Qwik 框架在特定条件下会信任并反序列化用户可控的输入,并且能够通过特定的序列化格式,动态加载 Node.js 核心模块(如 child_process)并调用其中的危险函数。
详细技术分析
攻击面与触发条件
攻击需要满足以下条件才能成功触发:
- 目标服务器运行着 Qwik ≤ 1.19.0 版本的应用。
- 目标应用至少存在一个有效的页面路由。
- 攻击者能够向该路由发送一个特制的 POST 请求。
漏洞利用链分解
整个攻击流程遵循以下链条:构造恶意请求 → 触发反序列化 → 绕过校验 → 动态加载模块 → 执行系统命令。
第一步:构造恶意 HTTP 请求
一个能够触发漏洞的完整 HTTP 请求示例如下:
POST /some-page?qfunc=exec HTTP/1.1
Host: victim.com
Content-Type: application/qwik-json
X-QRL: exec
Content-Length: 87
{
"_objs": ["child_process#exec[['whoami']]"],
"_entry": "0"
}
关键参数解释:
- 路径 (
/some-page): 必须是目标服务器上真实存在的页面路由。攻击者通常需要先对目标进行路径枚举。 - 查询参数 (
?qfunc=exec): 此处的值exec需要与后面请求体中的恶意函数名,以及X-QRL请求头的值匹配。它是服务端查找和验证要执行的函数的依据之一。 - 请求头 (
X-QRL: exec): 其值必须与qfunc参数的值完全相同,这是框架进行初步校验的环节。 - 请求头 (
Content-Type: application/qwik-json): 这是触发框架进入特殊反序列化路径的关键,如果类型不是此值,请求体会被当作普通数据处理。 - 请求体 (
_objs数组): 这是载荷的核心。数组中的字符串child_process#exec[['whoami']]是一个 QRL 序列化字符串,它指定了要加载的模块(child_process)、要调用的函数(exec)以及传递给函数的参数(['whoami'])。
第二步:后端处理流程与漏洞触发点
- 请求接收与路由:请求首先进入
packages/qwik-city/src/middleware/node/index.ts的中间件。该处将原生 Node.js 请求转换为 Qwik 内部格式,并注入反序列化器 (qwikSerializer)。 - 路由匹配:在
packages/qwik-city/src/middleware/request-handler/resolve-request-handlers.ts中,框架尝试匹配请求路径。如果匹配到一个已存在的页面路由,runServerFunction处理函数会被自动添加到处理链中。 - 校验与解析:在
runServerFunction函数中,框架执行以下检查:- 从 URL 查询参数中获取
qfunc的值(记为fn)。 - 验证请求头
X-QRL的值是否等于fn。 - 验证
Content-Type是否为application/qwik-json。
如果全部通过,则调用ev.parseBody()解析请求体,从而进入反序列化流程。
- 从 URL 查询参数中获取
- 触发反序列化:在
request-event.ts中,由于Content-Type匹配,框架直接调用qwikSerializer._deserializeData对请求体进行反序列化。 - 解析 QRL 字符串:在反序列化器的
_deserializeData方法中,会遍历_objs数组。当遇到以特殊前缀(⌥)开头的字符串时,会调用parser.prepare进行处理,最终进入parseQRL函数。 - 解析载荷结构:
parseQRL函数按照特定格式解析字符串child_process#exec[['whoami']]:chunk:"child_process"(模块名)symbol:"exec"(函数名)captures:["['whoami']"](函数参数)
- 创建 QRL 对象:解析后的参数被传递给
createQRL函数,创建一个 QRL 对象。此对象包含一个关键的getHash()方法,用于返回该 QRL 的哈希标识。哈希的计算规则(见getSymbolHash函数)是:提取symbol字符串中最后一个下划线 (_) 之后的部分;如果没有下划线,则返回整个symbol。对于symbol = "exec",其哈希值hash = "exec"。
第三步:绕过执行校验
创建 QRL 对象后,流程回到 runServerFunction。此处存在一个关键校验:
if (isQrl(qrl) && qrl.getHash() === fn) {
// 执行 QRL
}
此校验意图是确保要执行的 QRL 对象与请求中声明的 (qfunc=fn) 是同一个。然而,这里的校验逻辑是用 QRL 对象的哈希值与 URL 参数 fn 进行比较。
在我们的攻击载荷中,qrl.getHash() 返回 "exec",而 URL 参数 ?qfunc=exec 使得 fn 的值也是 "exec"。因此,校验通过,程序继续执行这个 QRL 对象。
第四步:动态模块加载与命令执行
- 解析模块路径:执行 QRL 时,会调用
importSymbol函数来动态加载模块。该函数会将我们指定的chunk("child_process") 补上.js后缀,变为"child_process.js",然后尝试通过 Node.js 的require函数加载。 - 查找并调用函数:
require("child_process.js")实际上会加载 Node.js 内置的child_process核心模块。接着,框架会尝试从这个模块对象中查找名为symbol("exec") 的导出。child_process.exec函数是真实存在的,因此被成功找到并返回。 - 最终执行:返回的
child_process.exec函数被调用,参数为我们通过captures指定的['whoami']。至此,系统命令whoami在服务器上成功执行,攻击完成。
关于错误示例的说明:文档中提到了一个错误示例 child_process#exec_abc123[['whoami']]。这个载荷的问题在于,它生成的 symbol 是 "exec_abc123",其哈希值 hash 为 "abc123"。虽然这样能通过 qrl.getHash() === fn 的校验(只需设置 ?qfunc=abc123),但在 importSymbol 阶段,框架会在 child_process 模块中查找名为 "exec_abc123" 的导出,而这个导出并不存在,导致攻击失败。因此,正确的攻击载荷中,symbol 必须是目标模块中真实导出的函数名。
漏洞修复
在 1.19.1 版本中,官方修复了此漏洞。主要的修复点在于修改了 importSymbol 函数(或相关逻辑),限制了动态加载模块的范围,禁止了通过此类反序列化操作任意加载 Node.js 核心模块(如 child_process, fs, os 等)或文件系统上其他危险模块的能力,从根本上杜绝了通过此途径实现远程代码执行的可能。
缓解与防范建议
- 立即升级:所有使用 Qwik 框架的项目应立即升级到 1.19.1 或更高版本。这是最根本、最有效的解决方案。
- 输入验证与过滤:对于无法立即升级的环境,应在网关、WAF 或应用层中间件上,对传入的请求进行严格检查。可考虑拦截或阻断满足以下特征的请求:
Content-Type: application/qwik-json- 包含
X-QRL请求头 - 查询字符串中包含
qfunc参数 - 请求体中含有疑似 QRL 序列化字符串(如包含
#和[的特定格式)。
- 网络隔离:运行受影响应用的服务应处在严格的网络策略之下,限制其出网和横向移动的能力,以降低漏洞被利用后的影响。
- 最小权限原则:运行 Qwik 应用的进程应遵循最小权限原则,避免使用 root 或高权限账户运行,以限制命令执行成功后攻击者获得的权限。
参考链接
- 官方安全公告:https://github.com/QwikDev/qwik/security/advisories/GHSA-p9x5-jp3h-96mm