一次同秒竞态,如何把污染制品送进 npm 与 TestPyPI

一、为什么"一秒"值得写成一篇文章

在普通业务系统里,时间戳相差一秒可能只是日志排序问题;在自动发布系统里,它可能决定哪份代码获得包仓库身份。

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 元数据

随后为每条路径回答:

  1. 谁能触发?

  2. 谁能修改被执行的代码?

  3. 审批绑定的是分支还是 SHA?

  4. 哪个 Job 生成 Artifact?

  5. 哪个 Job 消费并执行它?

  6. 消费者拥有哪些 Token 与 OIDC 权限?

  7. 包发布是否需要独立环境审批?

  8. 发布物能否对应到受信提交和摘要?


十二、处置历史暴露风险

如果组织发现存在同类流程,除了修复 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 获得高权限发布结果。

相关推荐
贾伟康1 小时前
【HarmonyOS 7新能力|037】数字盾工程封装:把接入逻辑放进可维护的分层结构
安全·harmonyos·arkts·软件架构·数字签名
m0_715674432 小时前
智能化+基于行标+场景化 政务数据库审计与风险监测全维度解决方案
网络·数据库·安全·网络安全·政务
山东科恩光电2 小时前
安全触边系统在工业自动化设备中的重要性及应用前景
安全
看浪的路人2 小时前
第8讲:运行时安全与沙箱隔离——Agent 能做什么,不能做什么
安全
云杂项3 小时前
Safety at Scale: A Comprehensive Survey of Large Model and Agent Safety(LLMs章节)
人工智能·安全
终端安全笔记3 小时前
iOS 27 给了租赁一个新工具,但它只认受监督的设备
android·网络·安全·ios
jimmyleeee3 小时前
大模型安全之二十一:构建 AI 安全控制栈:从威胁映射到持续合规的完整指南
人工智能·安全
漫话明说4 小时前
工智能安全描述性综述(2026):基于30份行业报告的梳理
安全·智能体·ai安全
xiaoqiMikko4 小时前
Jetty 又出一条走私(CVE-2026-19203):官方叫升 9.4.64,而 9.4.64 在 Central 上是 404
java·安全