Repository navigation
Full-SHA lock entries can accept a fork-only commit under the upstream action name #174
Description
Congrats @nodeselector for bringing it into public preview!
While testing fork pull requests, we found a narrow mismatch between fresh lock generation, gh actions-lock --verify, and backend Require lockfile enforcement for actions pinned to a full SHA.
Summary
A contributor can change a workflow from a tagged action to a raw SHA that exists only in a fork of that action, then manually edit actions.lock to use the same SHA while retaining the upstream repository IDs and a legitimate upstream ref value.
Fresh gh actions-lock generation correctly rejects that commit as absent from upstream history. However, gh actions-lock --verify accepts the manually edited lock, and a hosted runner with an active Require lockfile policy executes the fork-only commit through the upstream action name.
We reproduced this with actions/checkout in a public sandbox PR:
- Consumer PR: Research: test fork-only actions/checkout SHA with hand-edited lock boost-sandbox/checkout-imposter-pr-lab-20261015#1
- Successful policy-protected run: https://github.com/boost-sandbox/checkout-imposter-pr-lab-20261015/actions/runs/38806105222
- Fork-only commit: vikorium/checkout@373d742
The action commit adds only a harmless log marker. It was never pushed or proposed to upstream actions/checkout.
Tested versions
gh-actions-lockv0.0.0-20261015144100-f43e069f9e87, source commitf43e069f9e87f8fedcc8f4fb69c617e3c17fe2af- GitHub-hosted runner
2.337.0 - Active
Require lockfilepolicy applying to all workflows, with no exclusions
Reproduction
-
Fork
actions/checkoutand create a commit that exists only on a fork branch. Our test commit is373d742f768e37e4e68d84ada06be93fb2ce64d8onvikorium/checkout:research/fork-only-runtime-marker. -
In a fork PR against a consumer repository, change:
uses: actions/checkout@v6.1.0to:
uses: actions/checkout@373d742f768e37e4e68d84ada06be93fb2ce64d8- Manually edit the lockfile as follows. The IDs are the genuine upstream
actionsorganization andactions/checkoutrepository IDs:
workflows:
'.github/workflows/checkout.yml':
- 'actions/checkout@373d742f768e37e4e68d84ada06be93fb2ce64d8'
dependencies:
'actions/checkout@373d742f768e37e4e68d84ada06be93fb2ce64d8':
ref: 'v6.1.0'
commit: 'sha1-373d742f768e37e4e68d84ada06be93fb2ce64d8'
owner_id: 44036562
repo_id: 197814629- As a control, fresh lock generation rejects the workflow and writes nothing:
Pinned commit not in upstream history actions/checkout@373d742f768e37e4e68d84ada06be93fb2ce64d8
commit 373d742f768e is not on any tag or branch of actions/checkout: it may be a fork or impostor commit served under the upstream name
- With the manual lock in place,
gh actions-lock --verifyaccepts it:
Scanning 1 workflow
1 action is pinned to a bare SHA without a tag ref
All 1 workflow valid
- Open the fork PR against a repository with Require lockfile enabled. The hosted run succeeds and records:
Secret source: None
Using locked action versions from the workflow's lockfile
Download action repository 'actions/checkout@373d742f768e37e4e68d84ada06be93fb2ce64d8' (SHA:373d742f768e37e4e68d84ada06be93fb2ce64d8)
Run actions/checkout@373d742f768e37e4e68d84ada06be93fb2ce64d8
RESEARCH_CHECKOUT_FORK_ONLY_COMMIT
The marker also appears during the action's post phase.
Expected behavior
--verify and hosted Require lockfile enforcement should reject a full-SHA lock when the commit exists only in a fork and is not reachable from any upstream actions/checkout branch or tag. This would match fresh lock generation and the README statement that locked commits must have an upstream branch containing them.
Actual behavior
--verify reports the workflow valid, and the hosted runner accepts the lock and executes the fork-only object under the actions/checkout name.
Scope and limits
We acknowledge that this attack scenario does requires a contributor to make visible changes to both the workflow and lockfile, including a raw full SHA (so sort of social engineering, possible enough to fooling into merging to default branch). It does not replace an unchanged, reviewed pin. Our test used a normal fork pull_request job with no Actions secrets and a read-only token (Contents: read, Metadata: read).
A symbolic-ref control did reject the same fork-only commit: actions/checkout@main with a lock pointing to that SHA failed in both the CLI and runner. The observed gap is specific to a manually authored full-SHA lock entry.
Source observation
In internal/pipeline/parity.go, the parity check attaches Head only when the lockfile key ref is not a full SHA:
case verify && !parserlock.IsFullSha(o.lp.Pin.Ref):
pins[i].Head = o.lp.Pin.RefFor the manually authored entry above, the key ref is the full SHA, so verification checks repository identity and object existence without an upstream ancestry check. Because repositories in a fork network share Git objects, the fork-only object can resolve through the upstream repository name.
This does not appear to be listed as a documented limitation. The current README says lockfiles are checked for forgeries, that a SHA must exist in its stated refs, and that a locked action must have a branch containing the commit. The Limitations section currently mentions only local-path actions.
Suggested fix and regression coverage
For full-SHA lockfile keys, verify that the commit is reachable from the upstream ref recorded in the lock entry, or otherwise prove that at least one upstream branch or tag contains it, as fresh generation already does. The hosted lockfile enforcement path should apply the same provenance rule.
A regression test could construct a full-SHA lock entry with valid upstream repository IDs where the object is resolvable through the fork network but exists on no upstream ref, then assert that both --verify and runner enforcement reject it. A corresponding full SHA reachable from an upstream branch should remain valid.
Activity
nodeselector commentedon Oct 16, 2026
@fproulx-boostsecurity thank you for the report. I have fixed the code defect that allowed the bypass on the backend. I'll come back to the gap in the cli next week.
Align stub scenarios with recorded-ref proof
Lock live checkout fixtures to a commit their v4 ref contains
nodeselector commentedon Oct 20, 2026
I am also fixing gaps where you could use git describe and revision syntax to bypass the protections on both the backend resolver and the CLI. Backend is fixed and the CLI fix for those are in progress.
Repair SHA pins to any recorded ref at the commit