欢迎光临
我们一直在努力

源码之外还有攻击面:Argos CI 分支名命令注入复盘

导语

很多团队的 Pull Request 安全流程,第一步是审查代码 diff,第二步是让自动化工具扫描依赖与源码。

这套方法默认了一件事:攻击者能控制的主要内容都在仓库文件里。

现实并非如此。一次 CI 运行还会消费分支名、标签名、仓库名、提交信息、PR 标题、作者身份、构建矩阵和平台环境变量。它们不一定出现在代码 diff 中,却会进入 Shell、路径、模板、缓存和部署配置。

Argos CI 的 CVE-2026-59960 展示了这种“仓库外输入”如何变成供应链执行入口:攻击者可影响的分支或 ref 被读取为 CI 元数据,随后拼入 execSync() 的 Git 命令字符串。特定配置下,Shell 会在 Git 启动前解释该值。

本文把重点放在漏洞背后的威胁模型:为什么只扫描源码不够,以及团队如何为 CI 建立一张真正完整的输入地图。


一、事件与版本边界

已确认事实

  • CVE-2026-59960,GHSA-4×45-gxvp-6283;

  • 影响 @argos-ci/core 6.2.0 及更早版本;

  • 最低修复版本为 6.2.1;

  • CVSS 3.1 为 7.5,High;

  • 漏洞类型为 CWE-78 命令注入;

  • 输入可来自 GITHUB_HEAD_REF 或 ARGOS_BRANCH;

  • 当 hasRemoteContentAccess: false 时,上传流程可能在本地调用 Git 计算 merge base;

  • 项目于 2026 年 6 月 21 日发布修复,GitHub 已审核漏洞库于 9 月 10 日收录。

证据边界

一手来源没有确认在野利用。本文讨论的凭据泄露、制品污染等属于成功执行后结合 CI 权限模型得出的风险,不是已确认事故事实。


二、CI 的输入比仓库目录更大

可以把流水线输入分为五层:

1. 内容层

  • 源码;

  • 测试;

  • 构建脚本;

  • 项目配置;

  • 锁文件。

2. Git 元数据层

  • branch/ref;

  • tag;

  • commit SHA;

  • 提交作者与信息;

  • 仓库和组织名称。

3. 协作平台层

  • Pull Request 标题和正文;

  • Issue 内容;

  • 标签;

  • 评论;

  • 贡献者身份与 fork 信息。

4. CI 编排层

  • 事件类型;

  • 构建矩阵;

  • job 输出;

  • reusable workflow 输入;

  • 环境变量和缓存键。

5. 外部服务层

  • 制品仓库响应;

  • 扫描平台返回值;

  • 部署目标;

  • 云身份声明;

  • 第三方 API 数据。

安全扫描通常覆盖第一层,命令注入却可能来自第二或第四层。CVE-2026-59960 的价值,正是让这些盲区变得具体。


三、一次“元数据升级”的过程

分支名本身只是字符串。危险来自它在系统中不断更换角色:

Git ref 标识


Pull Request 事件字段


GITHUB_HEAD_REF 环境变量


Argos config.branch


Git 命令模板的一部分


Shell 源代码

前四步都可以是合法的数据传输。最后一步改变了信任性质:应用不再把 ref 交给 Git,而是先让 Shell 解释一段混合了固定代码和外部数据的字符串。

这就是注入漏洞的共同结构:

数据不是因为包含“坏字符”才危险,而是因为程序把它放进了会重新解释字符的上下文。


四、为什么代码 diff 可能完全正常

设想一个外部贡献流程:

  • 贡献者在 fork 中创建分支;

  • 提交内容只修改普通文档或截图;

  • Pull Request 触发视觉回归任务;

  • Argos SDK 读取 head ref;

  • 本地 merge-base 逻辑执行 Git 命令;

  • ref 在 Shell 中获得额外语义。

  • 代码审查者看到的可能只是无害文件。SAST 扫描的是新提交中的源码,也很难发现第三方依赖内部如何消费事件元数据。

    所以,传统问题“这次 PR 新增了什么危险代码?”需要补充为:

    这次工作流还处理了哪些由贡献者控制、但不在 diff 中的字段?


    五、配置条件决定路径,而权限决定后果

    官方公告指出,hasRemoteContentAccess: false 时,Argos 会在本地计算 merge base。未连接 Git 提供商集成的项目会处于这一状态。

    这要求安全团队把漏洞评估拆成两个维度。

    可达性

    • 受影响版本是否实际运行;

    • Argos 上传步骤是否触发;

    • 外部主体是否可影响 branch/ref;

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

    影响上限

    • Runner 是否能读取 Secret;

    • Token 是否有写权限;

    • 是否可以申请云 OIDC 身份;

    • 是否能发布包或镜像;

    • 是否复用持久工作区;

    • 是否接触签名材料。

    同一漏洞在无凭据的一次性 Runner 和拥有发布权限的自托管 Runner 上,风险结果完全不同。


    六、补丁体现了正确的边界重构

    修复前:

    execSync(`git merge-base ${head} ${base}`);

    修复后:

    execFileSync("git", ["merge-base", head, base]);

    前者创建 Shell 程序文本,后者创建进程和参数数组。

    官方补丁覆盖四处相似调用:gitFetch、gitMergeBase、listShas 和 listParentCommits,并增加回归测试,证明包含 Shell 特殊语义的 ref 不会产生额外文件副作用。

    这给代码审计带来两个直接启示:

  • 修复注入时应移除解释器,而不是只做字符过滤;

  • 一个 sink 被发现后,应搜索所有同类调用做变体分析。


  • 七、无害复现实验:建立 CI 输入地图

    下面的代码不会调用 Git 或 Shell,只把不同输入和汇点登记出来:

    inputs = {
    "source_files": "external contributor",
    "branch_ref": "external contributor",
    "pr_title": "external contributor",
    "workflow": "base repository maintainer",
    "secret": "repository administrator",
    }

    sinks = {
    "source_files": ["compiler", "linter"],
    "branch_ref": ["git command", "cache key"],
    "pr_title": ["log", "notification template"],
    "workflow": ["CI orchestrator"],
    "secret": ["environment"],
    }

    for name, owner in inputs.items():
    print(f"{name:12} controlled_by={owner:24} sinks={sinks[name]}")

    团队可以把它改成表格或资产清单。关键不是运行代码,而是强迫设计者回答:每个字段由谁控制,会被哪个解释器消费。


    八、工作流审计方法

    第一步:列出触发事件

    检查 pull_request、pull_request_target、workflow_run、手工触发和复用工作流。

    第二步:列出攻击者可控字段

    包括 head ref、仓库名、PR 标题、标签、提交消息和矩阵输入。

    第三步:追踪汇点

    搜索:

    exec(
    execSync(
    spawn(…, { shell: true })
    sh -c
    bash -c
    eval
    模板渲染
    动态路径
    缓存键

    第四步:绘制权限

    记录每个 job 的:

    • GITHUB_TOKEN 权限;

    • 可见 Secret;

    • OIDC 权限;

    • 网络访问;

    • 文件系统与缓存;

    • 发布和签名权限。

    第五步:验证边界

    测试特殊输入是否保持为单独参数,错误是否只由目标程序返回,以及是否不存在额外副作用。


    九、外部 Pull Request 的安全分层

    推荐把流程拆成三层。

    层一:不可信检查

    • 无生产 Secret;

    • 只读仓库权限;

    • 一次性 Runner;

    • 禁止或严格限制出站网络;

    • 不发布产物。

    层二:可信构建

    • 仅对审核通过的不可变提交执行;

    • 重新检出固定 SHA;

    • 不继承不可信任务工作区;

    • 使用隔离缓存。

    层三:发布与签名

    • 只消费可信构建生成并验证的制品;

    • 使用短期身份;

    • 需要环境审批;

    • 不读取外部 PR 的动态 ref。

    这种分层能降低单个第三方工具漏洞对整个供应链的影响。


    十、排查与响应

    版本核验

    npm ls @argos-ci/core

    检查锁文件、全局 CLI、CI 镜像和缓存后的实际版本,修复目标为 6.2.1 或更高兼容版本。

    历史回溯

    • 找出受影响版本运行期间的外部 PR;

    • 保存 head ref、事件类型和任务日志;

    • 核对当时的 hasRemoteContentAccess;

    • 查找异常子进程、文件和网络连接;

    • 检查 Token、OIDC 和制品仓库审计记录;

    • 对异常构建产物重新验证来源。

    证据边界

    受影响版本存在,只能证明组件风险;路径可达才能证明暴露;异常副作用才能支持执行判断;凭据使用或产物变更才构成影响证据。


    十一、P0—P2 行动清单

    P0

    • 升级 @argos-ci/core 至 6.2.1+;

    • 暂停高权限工作流处理外部 ref;

    • 移除外部 PR 任务中的 Secret 与写权限;

    • 核对 pull_request_target 工作流;

    • 重新构建并验证实际制品版本。

    P1

    • 清点所有 CI 元数据输入;

    • 将 Shell 调用改成程序与参数分离;

    • 对自托管 Runner 做网络与文件系统隔离;

    • 按信任等级拆分缓存;

    • 监控非预期子进程和出站连接。

    P2

    • 将元数据污点追踪加入代码审查规范;

    • 为共享工作流建立安全基线;

    • 把“无代码变更”场景纳入红队测试;

    • 要求第三方 Action 和 SDK 进入 SBOM;

    • 量化外部 PR 中可见 Secret、写权限和持久资源。


    十二、总结

    CVE-2026-59960 的意义不只是一处 execSync() 使用错误。它让我们看到:现代流水线的输入边界早已超出仓库目录。

    Git ref 是元数据,但由外部主体控制;CI 环境变量是结构化字段,但仍可能包含特殊语义;第三方 SDK 是依赖,却能在高权限 Runner 上决定如何解释这些值。

    修复 Argos 的直接方式是升级到 6.2.1。更长期的改进,是把源码、配置、Git 元数据和平台事件统一视为外部输入,并为它们建立“控制者—传递链—解释器—权限”的完整地图。

    只审查 diff,已经不足以保护软件供应链。


    事实、推断与建议边界

    事实

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

    • branch/ref 可进入字符串形式的 Git 命令;

    • 特定配置下会到达 Shell 汇点;

    • 官方补丁用参数数组消除 Shell 解释。

    推断与建议

    • 凭据和制品风险取决于 Runner 权限;

    • 应将元数据纳入污点分析;

    • 应分离外部检查、可信构建与发布阶段。

    未知

    • 一手来源未确认在野利用;

    • 具体环境是否失陷必须依靠行为证据判断。

    赞(0)
    未经允许不得转载:171主机测评 » 源码之外还有攻击面:Argos CI 分支名命令注入复盘
    分享到: 更多 (0)

    评论 抢沙发

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