博客 / 实用指南

GhostAction:删除密钥不等于吊销密钥

GhostAction 会挖掘旧的密钥。请把密钥轮换与 Git 历史清理分开处理,并核实已泄露的凭据和工作流注入路径都已被封堵。

已完成信息安全方向博士研究生学业已通过校内答辩 · 最终学位论文答辩尚待进行

文件已经干净了,签发方怎么说?

您删除了密钥,但吊销它的访问权限了吗?GhostAction 让这个问题变得迫在眉睫:被攻陷的维护者账户可以引入一个工作流,不仅搜索当前文件,还会翻查旧的提交。干净的工作区无法告诉您,它找到的凭据是否仍然有效。这个问题只有签发该凭据的服务才能回答。

2026 年 10 月的这波攻击是本文的起点,但真正有用的产出,是一套能用证据结束事件的方法。我把问题拆成三个:密钥材料还残留在哪里,签发方是否仍然接受它,以及攻击者能否再引入另一个工作流。本文基于公开研究和 GitHub 官方文档,未在此运行任何攻击载荷。[1][2]

Git 历史成了被攻陷 Runner 的输入

StepSecurity 描述了一个以被攻陷维护者身份直接提交的恶意 security-audit.yml。它的 checkout 步骤会拉取完整历史,随后脚本用 git log -p --all 检查各个补丁。它还会把从现有工作流中发现的密钥名称填进去,试图收集已配置的 Actions 密钥值。名称告诉攻击者该请求什么,而值才是攻击者真正想要的。[1]

请看下面这段示意性的历史记录。提交 B 从最新文件中删除了令牌,而提交 A 中仍然保留着它。因此,能够检索历史的搜索可以找回旧的文本。不过,攻击者能收集到的内容仍受限于拉取到的引用、匹配到的模式以及载荷的输出上限。不要把这种能力夸大为“所有历史密钥都已被成功窃取”。

Illustrative repository history; no real credential

A: config.env contains API_TOKEN=DEMO_NOT_A_REAL_TOKEN
B: config.env no longer contains API_TOKEN

Current file: absent
Earlier commit: present
Issuer status: a separate question

找回的密钥还能通过认证吗?

能否被找回和是否仍然有效是两个相互独立的属性。一个旧令牌被吊销后可能依然可见,但已不再授予访问权限。一个令牌也可能已从您控制的仓库中消失,却仍能通过别人手中的副本使用。GitHub 建议先吊销或轮换已泄露的凭据;重写历史处理的是剩余副本,并需要协调克隆、分叉和各类引用。[2]

这一区别改变了我解读清理统计数据的方式。含有旧密钥文本的仓库数量衡量的是公开残留,而不是签发方仍会接受的凭据数量。它也改变了验收标准:如果旧凭据没有在签发方处被停用,仅仅替换 CI 变量是不够的。请把这一点与“构建在新凭据下能否正常运行”分开记录。

                              Old credential accepted?
                              Yes                 No
Secret text recoverable       Access remains       Residual disclosure
Secret text no longer found   Access may remain    Both checks needed

A repository scan cannot answer the issuer-status column.

到底哪项控制能拦住下一个工作流?

审查要求只有在被强制执行的地方才有用。GitHub 针对公共分叉工作流的审批控制,只覆盖这条贡献路径;它并不是针对维护者直接推送的通用审批关卡。StepSecurity 报告称,审批关卡拦下了部分已观察到的运行,但这并不能证明分叉设置可以保护每一个仓库免受此次攻击。[1][3]

对于受保护分支,应要求使用 Pull Request 并通过相应 Code Owner 的审查,然后检查谁可以绕过该规则。为 .github/workflows/ 设置 CODEOWNERS 条目明确了归属,而分支规则才让这项审查成为强制要求。还要保护 CODEOWNERS 文件本身,并检查其他分支——它们的 push 工作流同样可能获得密钥。只保护 main 分支,并不能覆盖所有可执行的工作流路径。[4][5]

只读的仓库令牌也可能伴随密钥窃取

收紧 GITHUB_TOKEN 的权限很有用,但它无法吊销另行传入作业的云凭据。所观察到的工作流可以在仓库内容为只读的情况下运行,同时收集其他凭据。因此,权限审查必须同时盘点内置令牌,以及提供给执行环境的每一个独立密钥。[1]

我的审查会沿着一条具体的链条进行:谁可以修改工作流,什么事件会触发它,作业获得了什么身份和密钥,以及它能连接哪些目标。对于发布类作业,要问的是:能编写工作流的人,能否把这个作业变成凭据读取器?限制仓库写权限只回答了这条链中的一个环节。这是一个架构审查问题,而不是断言每个 Runner 都泄露了全部密钥。

先控制访问,再整理仓库历史

在清理工作抹去上下文之前,先保留工作流版本、运行 ID 和相关审计记录。切断恶意执行路径,并吊销被攻陷维护者的凭据。然后盘点泄露范围内当前作业使用的密钥和历史凭据,为每一项指定负责人,并在签发服务处确认吊销。如果无法确认其有效性,应将其视为未解决问题,而不是宣布仓库已经干净。[1][2]

替换的凭据必须通过可信的执行路径部署,否则同一个被攻陷的工作流在下一次运行时就能读到它。历史清理本身需要额外的协调工作,而且无法收回攻击者已经拿到的副本。对于能够创建会话或其他凭据的密钥,还要审查由此衍生的访问;停用原始密钥,在逻辑上并不能证明没有建立其他独立的访问途径。

要一份结案记录,而不是一张干净的截图

修复记录应当让另一位工程师能够分清哪些工作已经完成、哪些只是假设。对于每一个泄露的凭据,记录其非机密标识、负责人、签发系统、暴露时间窗、吊销证据以及替换凭据的部署状态。同时记录注入所用身份是如何被控制住的,以及工作流路径是如何被收紧的。下面的表格是一份建议的审查材料,而不是本次事件的记录。

这样,委托方的问题就变得非常具体:您的团队能否证明,之前泄露的访问权限已不再被接受,并且新凭据不会通过同一条途径再次泄露?这正是我在修复验证中会要求的证据。它决定了您是仅仅关闭了一个工单,还是真正关闭了该工单本应消除的访问权限。

Credential ID / owner / issuer: ____________________
Exposure scope and evidence: _______________________
Old credential disabled at issuer: _________________
Replacement deployed through trusted workflow: _____
Compromised maintainer access contained: ____________
Other credentials or sessions investigated: _________
Remaining copies / uncertainty / responsible owner: _

参考来源

需要在您自己的产品中检查这个问题吗?

漏洞修复与修复验证

想学会自己发现这类漏洞吗?

安全代码审计课程(一对一)
全部文章

08联系

即将上线处理用户数据的产品?

给我发一份简要需求。我会在三天内回复问题或方案大纲,价格在开始前以书面形式确定。