FlawOpen

flawopen.com/Teardowns/cve-2026-84973-jupyterlab-snapshot-checkout-toctou

● CVE-2026-84973 · 高危
安全研究 · FlawOpen

CVE-2026-84973:JupyterLab 快照工作流 TOCTOU 漏洞

精度为一秒的时间检查可能让快照工作流检出并执行未经批准的 Pull Request 提交,且使用作业权限。请将 update-snapshots-checkout 升级至 1.0.0 或更高版本。

💡 通俗易懂的原理解析 (ELI5)

审查者批准了一个特定包裹,但收件处只核对时钟显示的秒数。如果有人在同一秒替换包裹,替换品也会被当成已批准。该 action 使用一秒精度的时间戳并允许相等值,因此 Pull Request 更新可能与维护者评论同时通过检查。

核心概念与专有名词

TOCTOU
Time-of-check to time-of-use:安全检查针对某个资源作出决定,但资源在实际使用前发生了变化。
Pull Request head SHA
Pull Request 分支顶端提交的不可变标识。工作流应只执行被明确授权的那个提交。
GitHub Actions 令牌权限
作业的 GITHUB_TOKEN 被允许执行的仓库操作,例如读取或写入内容和 Pull Request。
CWE-367 / CWE-829
公告将时间竞争归为 CWE-367,将执行由 Pull Request 控制的功能归为 CWE-829。

框架映射

根本原因剖析 (Root Cause)

在 1.0.0 之前,update-snapshots-checkout 获取 Pull Request 的 pushed_at 与触发评论的 created_at,将二者转换为 Unix 秒,并且仅在 pushed_at 严格大于时拒绝运行。两个 API 时间戳的精度都是一秒,因此同一秒内的推送和授权评论可能得到相等值并通过检查。随后 action 保存当前 head SHA 并在 checkout 后核对;这只能证明检出的提交等于刚获取的 SHA,不能证明维护者授权了该提交。上游修复将 -gt 改为 -ge,从而拒绝相等和更晚的推送。只有使用受影响版本且后续步骤会执行 Pull Request 内容的工作流才面临风险;影响范围取决于工作流权限和凭据。

攻击执行流程分解

步骤 1

可信维护者触发工作流

Pull Request 创建后,OWNER、COLLABORATOR 或 MEMBER 发布评论,启动快照更新工作流。

步骤 2

攻击者控制的更新与评论发生在同一秒

贡献者在同一秒更新 Pull Request 分支。API 只有一秒精度,因此转换后的 Unix 秒数可能相等。

步骤 3

严格大于比较放过相等值

有漏洞的 action 仅在 pushed_at 大于 created_at 时拒绝。相等值不满足条件,因此新的 head SHA 被记录。

步骤 4

工作流检出该提交并可能执行它

checkout 与记录的 SHA 相符,因此通过后续校验。如果之后的步骤运行 Pull Request 控制的 action 或脚本,它们会使用作业配置的权限和凭据。

步骤 5

升级并降低工作流权限

升级到 1.0.0 或更高版本。将 actions 固定到不可变 SHA,只授予 GITHUB_TOKEN 必需权限,并避免在特权作业中执行不可信 Pull Request 代码。

源代码对比:漏洞与安全实现

这是上游的一行修复:在 API 的时间精度下,-ge 会拒绝相等值,从而关闭已记录的同秒绕过。仍应将授权绑定到不可变 SHA 并缩减权限;时间先后关系不能替代不可变的授权决策。 上游 diff 保留原有诊断消息;消息写的是“之后”,但现在相等值也会被拒绝。

存在漏洞的实现
if [[ $(date -d "$pushed_at" +%s) -gt $(date -d "$COMMENT_AT" +%s) ]]; then
    echo "Updating is not allowed because the PR was pushed to (at $pushed_at) after the triggering comment was issued (at $COMMENT_AT)"
    exit 1
fi
加固后的安全修复
if [[ $(date -d "$pushed_at" +%s) -ge $(date -d "$COMMENT_AT" +%s) ]]; then
    echo "Updating is not allowed because the PR was pushed to (at $pushed_at) after the triggering comment was issued (at $COMMENT_AT)"
    exit 1
fi

工程与系统安全加固清单

参考来源