欢迎光临
我们一直在努力

一个 Git 分支名,如何穿透到 CI Runner 的 Shell?

导语

分支名通常被视为标签,而不是代码。

它出现在 Pull Request 页面、构建日志和发布记录中,看起来只负责回答“这次变更来自哪里”。但在 CI 系统中,分支名会被复制进环境变量、缓存键、文件路径和 Git 命令。如果其中某一步把它拼接进 Shell 字符串,标签就会突然获得程序语义。

Argos CI 的 CVE-2026-59960 正是这样一条数据流:攻击者可影响的 branch/ref 进入 GITHUB_HEAD_REF 或 ARGOS_BRANCH,被 Argos 当作普通字符串传递,最终进入 execSync() 构造的 Git 命令。由于 execSync() 会把完整字符串交给 Shell,ref 中的特殊语法可能在 Git 启动前被解释。

本文不提供真实攻击载荷,而是回答三个问题:分支名如何从元数据变成命令文本;为什么中间经过结构化 CI 事件仍不安全;以及怎样从根源上阻断这类数据流。


一、漏洞事实速览

已确认事实

  • CVE:CVE-2026-59960;

  • GHSA:GHSA-4×45-gxvp-6283;

  • 组件:npm 包 @argos-ci/core;

  • 影响范围:6.2.0 及更早版本;

  • 最低修复版本:6.2.1;

  • 严重度:High,CVSS 3.1 为 7.5;

  • 类型:CWE-78,操作系统命令注入;

  • 关键输入:CI branch/ref,例如 GITHUB_HEAD_REF 或 ARGOS_BRANCH;

  • 关键汇点:字符串形式的 execSync();

  • 关键条件:上传流程在 hasRemoteContentAccess: false 时走本地 merge-base 计算路径。

项目于 2026 年 6 月 21 日发布公告和修复版,GitHub Advisory Database 于 2026 年 9 月 10 日完成发布与审核。后者是漏洞进入更广泛数据流的时间,不是攻击发生时间。

尚未确认

截至本文核验时,官方一手来源没有确认在野利用,也没有披露受害组织。


二、第一道边界:谁真正控制分支名

在自有仓库中,分支命名通常受团队规范控制。但外部贡献场景不同:

  • fork 仓库的贡献者可以创建自己的分支;

  • Pull Request 事件会携带 head ref;

  • CI 平台将这些值暴露为上下文或环境变量;

  • 第三方 Action、SDK 和脚本继续消费这些字段。

因此,GITHUB_HEAD_REF 虽然由 GitHub Actions 提供,其内容仍可能源于外部贡献者。

这揭示了一个常见误区:

“值来自可信平台”不等于“值由可信主体决定”。

对安全审查而言,判断输入是否可信,应追溯其最初控制者,而不是只看最后一个传递它的系统。


三、第二道边界:字符串类型不提供语义隔离

官方公告给出的数据流中,分支信息经过 CI 环境对象和配置层,并被转换为字符串。

这能避免某些类型错误,却不能回答:

  • 它是否是合法 Git ref;

  • 它是否可以进入文件路径;

  • 它是否可以嵌入 URL;

  • 它是否可以写入 HTML;

  • 它是否可以安全拼入 Shell。

同一段字符串进入不同解释器,会获得不同含义:

最终汇点可能产生的语义
Git 参数数组 一个 ref 参数
Shell 命令字符串 命令、变量、重定向或替换语义
文件系统 API 路径与目录层级
HTML 模板 标签或脚本上下文
SQL 字符串 查询语法

所以,“结构化参数”和“安全数据”不是同义词。安全属性只能相对于最终解释器来定义。


四、第三道边界:execSync() 让数据进入 Shell

受影响代码的模式可简化为:

execSync(
`git fetch –force –depth ${depth} origin ${ref}:${target}`,
);

开发者想调用的是 Git,但操作系统首先调用的是 Shell:

JavaScript 模板字符串


完整命令文本


/bin/sh -c 解析

├── 解析 Shell 语法
└── 最后才启动 git

在这个模型中,${ref} 没有独立参数边界。它只是命令文本的一段字符。

最危险的地方并不是 Git 是否接受该 ref。即使 Git 最终因为 ref 无效而退出,Shell 的解析已经发生。因此,“CI 中的 Git 命令报错了”不能证明此前没有额外副作用。


五、为什么该路径不是每个环境都必然可达

漏洞描述明确指出,当项目的 hasRemoteContentAccess 为 false 时,Argos 上传流程会在本地计算 merge base,并调用相关 Git 辅助函数。

这意味着评估暴露面时,需要同时确认:

  • 是否安装受影响版本;

  • 是否运行 Argos 上传;

  • 是否处理攻击者可影响的 ref;

  • 是否进入本地 merge-base 路径;

  • Runner 是否执行受影响代码;

  • Runner 拥有哪些权限。

  • 官方说明,未连接 Git 提供商集成的项目会使用 hasRemoteContentAccess: false。它不是所有项目的统一状态,也不应被当成一个极其罕见的特例。


    六、补丁如何切断链路

    官方修复提交 8355f3a 将相关 Git 操作改成程序与参数分离的调用:

    execFileSync("git", [
    "fetch",
    "–force",
    "–depth",
    String(depth),
    "origin",
    `${ref}:${target}`,
    ]);

    修复后的路径是:

    不可信 ref


    参数数组中的单独元素


    直接启动 git

    └── 不经过 Shell 解释

    ref 仍然可能不符合 Git 语法,但它不会因为 Shell 元字符而变成额外命令。

    四个汇点一起修

    补丁没有只修改公开路径中的 gitFetch,还覆盖:

    • gitMergeBase;

    • listShas;

    • listParentCommits。

    这体现了正确的变体分析:围绕“字符串拼接后交给 Shell”搜索整个模块,而不是只修报告中的一行。


    七、无害模型:观察边界是否存在

    以下代码只构造数据结构,不启动 Shell、Git 或任何子进程:

    def vulnerable(ref: str) -> dict:
    return {
    "interpreter": "shell",
    "source": f"git fetch origin {ref}:refs/argos/head",
    }

    def fixed(ref: str) -> dict:
    return {
    "program": "git",
    "args": ["fetch", "origin", f"{ref}:refs/argos/head"],
    }

    branch = "UNTRUSTED_BRANCH_METADATA"

    print(vulnerable(branch))
    print(fixed(branch))

    缺陷设计中,ref 与命令合并成一段待解释源码;修复设计中,ref 保持为参数数组的一项。

    这是概念模型,不是 Argos PoC,也不包含攻击真实 Runner 的步骤。


    八、风险影响应如何准确表述

    已确认影响

    成功利用可使攻击者以 Argos 上传进程的权限执行操作系统命令。

    工程推断

    如果 Runner 同时拥有以下能力,后果可能扩大:

    • 读取仓库或组织 Secret;

    • 使用云平台 OIDC 身份;

    • 写入仓库或发布包;

    • 修改构建产物;

    • 访问持久缓存;

    • 控制自托管 Runner 或 Docker Socket。

    这些属于依据 CI 权限模型作出的风险推断,不代表官方已经确认相关资产被窃取或篡改。


    九、开发团队的修复清单

    P0

    • 将 @argos-ci/core 升级到 6.2.1 或更高兼容版本;

    • 更新锁文件并重新生成实际 CI 制品;

    • 用 npm ls @argos-ci/core 检查直接与传递依赖;

    • 核对全局安装的 @argos-ci/cli 实际携带的 core 版本;

    • 搜索内部代码中的 execSync、exec 和字符串式 Git 命令。

    P1

    • 对 branch、tag、SHA、路径和仓库名开展同类数据流审计;

    • 固定程序名并使用参数数组;

    • 输入校验只承担业务约束,不承担 Shell 转义;

    • 为特殊字符输入增加“不得产生副作用”的负向测试。

    P2

    • 建立统一的命令执行封装,默认禁止 Shell;

    • 在代码评审中标注每个子进程参数的控制来源;

    • 用静态规则发现模板字符串流入命令执行 API;

    • 将所有元数据源纳入污点分析,而不只追踪 HTTP 请求。


    十、安全与平台团队的工作流建议

    • 外部 PR 使用低权限、短生命周期 Runner;

    • 不为不可信任务注入生产 Secret;

    • 限制出站网络和可写目录;

    • 不向外部贡献任务挂载 Docker Socket;

    • 谨慎使用 pull_request_target;

    • 将检查、构建、签名和发布拆成不同信任阶段;

    • 让发布任务只消费经过验证的不可变制品,而不重新处理攻击者 ref。

    应监控的信号

    • 异常 branch/ref 字符;

    • Argos 上传前后的非预期子进程;

    • Git 命令异常退出与同期文件变化;

    • Runner 的异常出站连接;

    • CI Secret、OIDC 和制品仓库令牌的异常使用;

    • 相同自托管 Runner 上跨任务残留的文件与进程。


    十一、总结

    CVE-2026-59960 的攻击链跨越了三次身份变化:

  • 外部贡献者选择的分支名;

  • CI 平台提供的结构化环境变量;

  • Shell 解释器接收的程序文本。

  • 每次传递都没有改变字符串内容,却改变了人们对它的信任判断。真正的失守发生在最后一步:execSync() 抹掉了程序与参数的边界。

    修复的关键不是过滤更多字符,而是取消不必要的 Shell。对所有 DevSecOps 工具而言,同样的原则都成立:追溯数据最初由谁控制,确认它最终进入哪个解释器,并让参数始终保持参数。


    事实、推断与未知事项

    事实

    • 影响 @argos-ci/core <= 6.2.0,修复于 6.2.1;

    • ref 可从 CI 环境进入字符串式 execSync();

    • hasRemoteContentAccess: false 是公开路径的重要条件;

    • 补丁使用 execFileSync() 参数数组并覆盖四个汇点。

    推断与建议

    • 高权限 Runner 会提高潜在供应链影响;

    • 应隔离外部 PR、减少 Secret、禁用不必要 Shell;

    • 应把 Git 元数据纳入污点分析和安全测试。

    未知

    • 截至核验时,一手来源未确认在野利用;

    • 单个组织的真实暴露取决于版本、配置、事件类型和 Runner 权限。

    赞(0)
    未经允许不得转载:171主机测评 » 一个 Git 分支名,如何穿透到 CI Runner 的 Shell?
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址