FlawOpen

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

● CVE-2026-84973 · High
FlawOpen Security Research

CVE-2026-84973: JupyterLab Snapshot Workflow TOCTOU

A one-second timestamp check could let a snapshot workflow check out an unapproved pull-request commit and execute it with the job’s permissions. Upgrade update-snapshots-checkout to 1.0.0 or later.

💡 Plain English Explainer (ELI5)

A reviewer approves a specific parcel, but the delivery desk checks only the second on the clock. If someone swaps the parcel during that same second, the desk treats the replacement as approved. The action used timestamps with one-second precision and allowed equal values, so a pull-request update could slip through beside the maintainer’s comment.

Core Concepts & Subsystem Terms

TOCTOU
Time-of-check to time-of-use: a security decision is made about a resource, then the resource changes before it is used.
Pull-request head SHA
The immutable commit identifier at the tip of a pull request branch. A workflow should execute only the exact commit it intended to authorize.
GitHub Actions token permissions
The repository operations a workflow’s GITHUB_TOKEN is allowed to perform, such as reading or writing contents and pull requests.
CWE-367 / CWE-829
The advisory maps the timing race to CWE-367 and the execution of pull-request-controlled functionality to CWE-829.

Framework mappings

  • CWE reference for the weakness described in this guide: CWE-367 CWE-829

Root Cause Analysis

Before version 1.0.0, update-snapshots-checkout fetched the pull request’s pushed_at timestamp and the triggering comment’s created_at timestamp, converted each to epoch seconds, and rejected the run only when pushed_at was strictly greater. Both API timestamps have one-second precision. A push and authorized comment in the same second therefore compared equal and passed the guard. The action then saved the current head SHA and checked it out; that later equality check proved only that the checkout matched the newly fetched SHA, not that the maintainer had authorized that commit. The upstream fix changes the comparison from -gt to -ge, rejecting equal timestamps as well as later pushes. This affects consuming workflows only when they use a vulnerable action version and subsequently execute content from the checked-out pull request. The impact is bounded by that workflow’s permissions and available credentials.

Step-by-Step Attack Flow

Step 1

A trusted maintainer requests the workflow

A pull request is opened and a trusted OWNER, COLLABORATOR, or MEMBER posts the comment that triggers the snapshot-update workflow.

Step 2

An attacker-controlled update races the comment

A contributor updates the pull-request branch in the same second. The API timestamps have one-second precision, so the push and comment can produce equal epoch-second values.

Step 3

The strict greater-than check accepts equality

The vulnerable action rejects only when pushed_at is greater than created_at. Equal timestamps do not satisfy that condition, so it records the new pull-request head SHA.

Step 4

The workflow checks out and may execute that commit

The checkout matches the recorded SHA and passes the action’s later equality check. If later workflow steps run pull-request-controlled actions or scripts, they run with the job’s configured permissions and credentials.

Step 5

Upgrade and reduce workflow authority

Upgrade the action to 1.0.0 or later. Pin actions to immutable commit SHAs, grant only required GITHUB_TOKEN permissions, and keep untrusted pull-request code away from secrets and privileged jobs.

Source Code: Flaw vs. Secure Implementation

This matches the upstream one-line fix: -ge rejects equality at the timestamp precision available from the API. It closes the documented same-second bypass. Workflows should still pin the authorized commit directly where possible and minimize permissions because timestamp ordering is not a substitute for an immutable authorization decision. The upstream diff leaves the existing diagnostic message unchanged; its wording says after even though equality is now rejected.

Source excerpt: .github/actions/update-snapshots-checkout/action.yml — pushed_at comparison

UNPATCHED FLAW
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
HARDENED SECURE PATCH
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

Engineering & System Hardening Checklist

Sources