Skip to content

Harden pr-merge's run-cache restore in the token-holding jobs #9455

Open

Description

@kyle-sexton

Problem

Two jobs of .github/workflows/pr-merge.yml restore the pr-merge-run-cache-* actions/cache entry by bare prefix (restore-keys: pr-merge-run-cache-) after they hold the lanes App merge token:

Any job in the default branch's cache scope can write a newer key under that prefix. carry-run-cache.mjs already filters the restored JSON, so a poisoned entry's contents can only make the gate refuse; they cannot forge a pass.

Two risks are not covered:

  • Extraction path (UNVERIFIED). The security review of fix(pr-merge): require a pr-verify pass before a lane pull request merges natively #9450 raised this concern from published cache-poisoning research (Cacheract): actions/cache may extract a crafted archive with entries outside the declared path. If it does, a poisoned entry could overwrite files under .base/ (for example carry-run-cache.mjs or enable-auto-merge.mjs) before they run with the lane token and id-token: write. This has not been verified for actions/cache/restore v6.1.0.
  • Who can write the key. It has not been checked which default-branch jobs could be made to run attacker-influenced code that writes that key.

Proposal

Pick one, after verifying the extraction behaviour:

  1. Verify the archive. Restore into an empty directory in a step or job that holds no token. List the archive entries, refuse any path outside the single expected file, and only then copy the file into place.
  2. Move to the artifact-read pattern. Use the pattern perf(source-control): carry untied FAIL head refs between pr-merge runs #9416 uses for the head-ref map: an artifact uploaded only by a scheduled or hand-started run of main's pr-merge.yml, found through that run's record. That removes the shared cache scope from the trust path.

Either change should land in both jobs together.

Done when

  • The extraction behaviour of the pinned cache restore is verified, and the source is cited.
  • Neither token-holding job extracts a cache archive a non-pr-merge job could write, or the archive's paths are checked first.
  • carry-run-cache.mjs's content filter is kept.

Refs: #9450

Activity

  1. added
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    on Oct 23, 2026
  2. github-actions commented on Oct 23, 2026

    @github-actions
    Contributor

    Proposed work class: none

    Thanks for the report. Automated triage handed this issue to a maintainer.

    Unconfirmed model suggestion: work-class: structural
    Basis: automated intake triage; a maintainer confirms or changes it.

    Automated triage. A maintainer reviews it.

  3. added
    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.
    and removed
    needs-triageNot yet classified. Floor until a type and one priority tier are set.
    on Oct 23, 2026
  4. kyle-sexton commented on Oct 23, 2026

    @kyle-sexton
    MemberAuthor

    The claude-security-review lane on #9450 (#9450 (comment)) suggests an integrity check on the carried run cache: an HMAC, or a check that a main-branch pr-merge run saved it. It also suggests reading the cache back through the artifact path. Today the carried sections can only count a FAIL (carry-run-cache.mjs:9-11; Reader._run_state reads a PASS's run fresh), so this protects the refuse direction and the extraction path, not the pass direction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    needs-humanHuman-in-the-loop required; autonomous sessions must not resolve items carrying this.status: needs-decisionAwaiting a human or maintainer judgment call.

    Type

    Fields

    No fields configured for Task.

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions