Coordinated Disclosure Timeline
- 2026-08-27: A Private Vulnerability Report (PVR) was created in the jupyter/notebook repository.
- 2026-08-31: The report was acknowledged.
- 2026-09-03: The PVR was transferred to the jupyterlab/maintainer-tools repository.
- 2026-09-03: The
update-snapshots-checkoutaction was fixed in v1.0.0, and GHSA-wwhg-p79f-vfvm was published.
Summary
Versions of the jupyterlab/maintainer-tools update-snapshots-checkout action before 1.0.0 contain a time-of-check-to-time-of-use (TOCTOU) vulnerability. The action attempts to reject pull request updates made after an authorized snapshot-update comment by comparing the pull request head repository’s pushed_at timestamp with the comment’s created_at timestamp. Both timestamps have one-second precision, and the action rejects only a push that compares strictly later. A malicious push made in the same second as the authorized comment therefore compares equal and is accepted, allowing the action to check out a different, attacker-controlled commit from the one the maintainer intended to authorize. Any consuming workflow that subsequently executes code from that checkout may run attacker-controlled code with the workflow job’s permissions and secrets.
Project
jupyterlab/maintainer-tools update-snapshots-checkout action
Tested Version
The action at commit b48fcdb87e70c45789abd80d4042b06641e47b46.
Details
One-second TOCTOU in update-snapshots-checkout enables attacker-controlled checkout (GHSL-2026-203)
The update-snapshots-checkout composite action is intended for workflows that update Playwright snapshots in response to a pull request comment. It first retrieves the triggering comment’s author_association and rejects commenters who are not an OWNER, COLLABORATOR, or MEMBER. This authenticates the person invoking the workflow, but the action must also ensure that the pull request commit it checks out is the one that person intended to authorize.
To detect a push after the authorized comment, the vulnerable action fetches the pull request’s current head SHA and the head repository’s pushed_at timestamp, then compares pushed_at with the triggering comment’s created_at timestamp:
- name: Get PR Info
id: pr
shell: bash -l {0}
env:
PR_NUMBER: ${{ github.event.issue.number }}
GH_TOKEN: ${{ inputs.github_token }}
GH_REPO: ${{ github.repository }}
COMMENT_AT: ${{ github.event.comment.created_at }}
run: |
pr="$(gh api /repos/${GH_REPO}/pulls/${PR_NUMBER})"
head_sha="$(echo "$pr" | jq -r .head.sha)"
pushed_at="$(echo "$pr" | jq -r .head.repo.pushed_at)"
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
echo "head_sha=$head_sha" >> $GITHUB_OUTPUT
Both timestamps have one-second precision. If an attacker pushes a malicious commit in the same second as the authorized comment, both conversions to epoch seconds produce the same value. Because the condition uses strict greater-than (-gt) rather than rejecting equality, the action accepts the push and records the new, attacker-controlled head_sha.
The action then checks out the pull request branch and verifies that HEAD equals the SHA it just recorded:
- name: Checkout the branch from the PR that triggered the job
shell: bash -l {0}
env:
GITHUB_TOKEN: ${{ inputs.github_token }}
run: gh pr checkout ${{ github.event.issue.number }}
- name: Validate the fetched branch HEAD revision
shell: bash -l {0}
env:
EXPECTED_SHA: ${{ steps.pr.outputs.head_sha }}
run: |
actual_sha="$(git rev-parse HEAD)"
if [[ "$actual_sha" != "$EXPECTED_SHA" ]]; then
echo "The HEAD of the checked out branch ($actual_sha) differs from the HEAD commit available at the time when trigger comment was submitted ($EXPECTED_SHA)"
exit 1
fi
The equality check confirms only that the checkout matches the post-race SHA returned by the pull request API. It does not confirm that this is the commit the authorized commenter intended to approve, so it does not mitigate the same-second race.
Any dependent workflow is vulnerable if it executes files from the checked-out tree after calling an affected version of update-snapshots-checkout. The exact impact is determined by that workflow’s permissions, secrets, credentials, and subsequent steps.
Affected usage in jupyter/notebook
The jupyter/notebook Update Playwright Snapshots workflow was directly affected. It runs on created or edited issue comments, accepts comments containing please update snapshots, and grants the job write access to repository contents and pull requests:
on:
issue_comment:
types: [created, edited]
jobs:
update-snapshots:
permissions:
contents: write
pull-requests: write
if: github.event.issue.pull_request && contains(github.event.comment.body, 'please update snapshots')
steps:
- uses: jupyterlab/maintainer-tools/.github/actions/update-snapshots-checkout@b48fcdb87e70c45789abd80d4042b06641e47b46 # v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
After the vulnerable action checks out the pull request head, the workflow loads a local composite action from that tree:
- name: Base Setup
uses: jupyterlab/maintainer-tools/.github/actions/base-setup@b48fcdb87e70c45789abd80d4042b06641e47b46 # v1
- name: Build
uses: ./.github/actions/build-dist
Because .github/actions/build-dist/action.yml comes from the pull request head, the pull request author can modify that action to run arbitrary commands. The same workflow later runs jlpm against pull request-controlled package metadata, providing additional execution paths through package lifecycle and named scripts:
- name: Install the test dependencies
run: |
cd ui-tests
jlpm
jlpm playwright install
- name: Update snapshots
uses: jupyterlab/maintainer-tools/.github/actions/update-snapshots@b48fcdb87e70c45789abd80d4042b06641e47b46 # v1
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
npm_client: jlpm
test_folder: ui-tests
start_server_script: 'null'
update_script: test:update --browser ${{ matrix.browser }}
A representative exploit against this affected usage is:
- An external contributor opens a pull request whose current head appears suitable for snapshot generation.
- An
OWNER,COLLABORATOR, orMEMBERcreates a comment containingplease update snapshots. - The attacker uses an automated script to poll for new comments and pushes a malicious commit in the same second as that comment.
- When the job fetches the pull request, it observes the malicious head. The push and comment timestamps compare equal, so the
-gtguard accepts the update and records the malicious SHA. - The checkout matches that recorded SHA, passes the later equality check, and the workflow executes the attacker-controlled local action and package scripts with the job’s write-scoped token.
Using a highly optimized script, we verified several successful proof-of-concept runs exploiting this same-second race. The race remains unreliable because of the one-second timestamp granularity: the comment may be created near the end of a second, leaving insufficient time to alter the fork. Nevertheless, the successful runs demonstrated that a malicious push made in the same second as the authorized comment bypasses the timestamp guard and reaches the privileged attacker-controlled execution path.
Impact
Successful exploitation gives the external pull request author control of the commit checked out by a consuming workflow. If that workflow executes code from the checkout, the attacker gains arbitrary code execution in the job and can access credentials and secrets available to that code.
In the demonstrated jupyter/notebook usage, the job’s GITHUB_TOKEN had contents: write and pull-requests: write. Attacker-controlled code could therefore perform repository-content and pull-request operations allowed by those permissions, subject to branch protection and other repository policies. The proof of concept did not rely on unrelated repository secrets or the ability to modify a protected branch.
CWEs
- CWE-367: Time-of-check Time-of-use (TOCTOU) Race Condition
- CWE-829: Inclusion of Functionality from Untrusted Control Sphere
Resources
- GHSA-wwhg-p79f-vfvm
jupyterlab/maintainer-toolsfix commitjupyter/notebookaffected usage- GitHub Security Lab: Preventing pwn requests
CVE
- CVE-2026-84973
Credit
This issue was discovered and reported by GHSL team member @JarLob (Jaroslav Lobačevski).
Contact
You can contact the GHSL team at securitylab@github.com; please include a reference to GHSL-2026-203 in any communication regarding this issue.
