一、为什么"一秒"值得写成一篇文章
在普通业务系统里,时间戳相差一秒可能只是日志排序问题;在自动发布系统里,它可能决定哪份代码获得包仓库身份。
Marimo 旧工作流的判断逻辑近似为:
推送时间 > 授权评论时间 → 拒绝
否则 → 继续发布
两个时间都只有秒级精度。攻击者如果在同一秒推送新代码,两个值相等,新提交就可能绕过"授权后禁止更新"的检查。
接下来,自动化不是简单读取代码,而是执行构建脚本、生成软件包,并把输出交给拥有发布能力的任务。这使一个小型竞态变成供应链问题。
二、事件事实
-
2026 年 9 月 4 日:问题通过私有漏洞报告提交;
-
2026 年 9 月 8 日:Marimo 删除旧
marimo-bot工作流; -
2026 年 9 月 16 日:GitHub Security Lab 公开 GHSL-2026-226。
【已确认事实】 漏洞类型为 CWE-367 与 CWE-829,没有分配 CVE。
【已确认事实】 维护者称该工作流此前已经停用,因此修复采用删除流程,而不是发布 Marimo 新版本。
【证据边界】 没有一手资料确认攻击者现实利用该流程,也没有确认 npm 或 TestPyPI 上出现由此产生的恶意 Marimo 包。
三、攻击链第一段:抢到被批准的执行位置
1. 维护者批准旧提交
维护者查看 PR,并通过评论启动测试发布。
2. 攻击者同秒推送新提交
由于时间戳相等,严格大于检查没有拒绝。
3. 工作流重新读取当前 SHA
系统拿到攻击者的新提交,而不是评论产生时的旧提交。
4. 新提交中的代码被执行
工作流运行仓库内的:
-
本地复合 Action;
-
Python 构建和修改脚本;
-
Shell 校验脚本;
-
Python 与前端构建后端。
这些文件均可能被 PR 作者修改。
四、攻击链第二段:生成两类污染 Artifact
构建任务把输出分成两类:
Python wheel Artifact
前端 / npm Artifact
Artifact 把低权限任务和高权限任务连接起来。
它的名称看起来合法、上传动作也来自正常工作流,但内容由攻击者控制的构建代码产生。因此发布任务不能因为来源于"同一套 CI"就默认信任。
这里的安全判断应是:
Artifact 是否来自批准 SHA?
生产者是否执行不可信代码?
内容摘要是否与受信重建一致?
消费者是否会执行其中的文件?
五、TestPyPI 路径:污染 wheel 被直接发布
下游 TestPyPI 任务下载 wheel Artifact,并使用具有 id-token: write 的发布流程上传。
风险链为:
不可信提交
→ 不可信构建脚本
→ 攻击者控制 wheel
→ 特权发布任务下载
→ TestPyPI
研究同时指出,该 Job 声明了 environment: testpypi。如果项目为这个环境配置了 Required Reviewers,仍可能存在额外审批。
【证据边界】 环境保护规则不是公开工作流文件可以完整证明的配置,因此不能断言 TestPyPI 发布一定能在无人批准的情况下完成。
六、npm 路径:不仅发布,还可能执行脚本
npm 路径下载前端 Artifact,然后执行 npm publish --provenance。
GitHub Security Lab 指出,该命令没有使用 --ignore-scripts。攻击者控制的 package.json 可以包含生命周期脚本,于是风险变成:
污染 Artifact 解压到发布目录
↓
npm publish 处理 package.json
↓
生命周期脚本在发布任务中执行
↓
任务拥有 id-token: write
这比"发布恶意包"更进一步:不可信代码可能在包发布 Job 本身执行。
同时,npm 发布 Job 没有声明 Environment,因此不能自动获得 Environment Required Reviewers 提供的那层保护。
七、id-token: write 到底意味着什么
它并不是一个可直接下载、长期保存的 npm 密码。
它允许 GitHub Actions Job 请求 OIDC Token,再由包仓库根据 Trusted Publishing 配置判断是否授予发布权限。
因此风险取决于:
-
OIDC 信任策略绑定了哪个仓库;
-
是否限制工作流和环境;
-
是否允许当前事件与引用;
-
Token 能在什么范围内使用;
-
发布 Job 中的不可信代码能否主动请求并使用身份。
【准确表述】 漏洞可让攻击者代码进入具备申请 OIDC 身份能力的任务,不应写成"攻击者偷到了永久 npm Token"。
八、为什么 --provenance 也没有阻止
--provenance 可以为发布包生成来源证明,但它不会回到维护者评论时判断"这是不是当时批准的提交"。
如果发布工作流真实运行、OIDC 身份真实、制品真实来自该任务,证明可能完全有效。
问题在于:
被证明的构建 ≠ 被维护者授权的构建
因此需要把 provenance 与审批 SHA、受信工作流和制品摘要一起验证。
九、无害双通道模型
下面的代码不执行命令、不生成包,只模拟两条发布通道的策略。
from dataclasses import dataclass
@dataclass
class BuildOutput:
source_sha: str
digest: str
contains_scripts: bool
def release(output: BuildOutput, approved_sha: str, channel: str):
if output.source_sha != approved_sha:
raise ValueError("source commit was not approved")
if channel == "npm" and output.contains_scripts:
raise ValueError("publishing must not execute package scripts")
return f"approved {channel} release: {output.digest}"
poisoned = BuildOutput("commit-B", "sha256:demo", True)
release(poisoned, "commit-A", "npm") # 拒绝
同一份 Artifact 在不同生态中具有不同隐式执行风险,因此发布策略也不能完全相同。
十、如何修复两条发布路径
共同措施
-
审批绑定精确提交 SHA;
-
PR 更新后重新审批;
-
高权限阶段从批准 SHA 重新构建;
-
Artifact 下载到隔离目录;
-
验证文件清单、摘要、来源和构建材料;
-
不信任 Artifact 内提供的分支名、SHA 或发布目标。
Python / TestPyPI 侧
-
在干净环境重新构建 wheel;
-
检查 wheel 文件列表和元数据;
-
为 TestPyPI Environment 配置 Required Reviewers;
-
将发布身份绑定到特定工作流与环境;
-
正式发布前从可信源码重复验证。
npm 侧
-
发布阶段禁用不必要的生命周期脚本;
-
在只包含预期文件的干净目录发布;
-
检查打包清单和
package.json; -
为发布 Job 增加受保护 Environment;
-
收紧 Trusted Publishing 的工作流与环境条件。
十一、怎样排查自己的仓库
搜索以下组合:
issue_comment + publish
workflow_run + download-artifact
pull_request_target + checkout PR
id-token: write + 外部输入
npm publish + 未限制 scripts
Artifact + 分支名/SHA 元数据
随后为每条路径回答:
-
谁能触发?
-
谁能修改被执行的代码?
-
审批绑定的是分支还是 SHA?
-
哪个 Job 生成 Artifact?
-
哪个 Job 消费并执行它?
-
消费者拥有哪些 Token 与 OIDC 权限?
-
包发布是否需要独立环境审批?
-
发布物能否对应到受信提交和摘要?
十二、处置历史暴露风险
如果组织发现存在同类流程,除了修复 YAML,还应回溯:
-
评论触发发布的历史记录;
-
PR 在审批前后短时间内的提交变化;
-
对应工作流运行的实际 SHA;
-
上传和下载的 Artifact 摘要;
-
TestPyPI、npm 或内部仓库的发布记录;
-
包版本、制品摘要与源码提交的对应关系;
-
OIDC 与环境审批日志。
发现无法对应到可信提交的包时,应暂停使用、撤回或标记版本,并从可信源码重新构建验证。
十三、P0---P2 行动清单
P0
-
暂停同类评论触发发布;
-
删除废弃工作流;
-
检查外部 PR Artifact 是否进入包发布;
-
禁止发布 Job 执行不必要的生命周期脚本。
P1
-
绑定批准 SHA 并在更新后重新审批;
-
特权发布阶段重新构建;
-
为 npm 与 TestPyPI 设置受保护环境;
-
隔离验证 Artifact;
-
收紧 OIDC Trusted Publishing 条件。
P2
-
对历史包进行源码---制品---证明对账;
-
为工作流文件启用 CODEOWNERS;
-
在流水线扫描中加入 TOCTOU 与跨权限 Artifact 规则;
-
把包生命周期脚本纳入发布安全基线;
-
演练包仓库发布身份被工作流逻辑滥用的场景。
总结
GHSL-2026-226 的价值,不在于告诉我们"一秒钟很危险",而在于展示一个微小判断如何沿自动化信任链逐级放大。
安全发布必须确保:维护者批准精确提交,高权限阶段只消费可信输入,npm 与 Python 发布分别处理各自的隐式执行风险,OIDC 身份绑定到受保护环境。缺少任何一环,低权限构建都可能借 Artifact 获得高权限发布结果。