Security & supply chain

GitHub Actions hands fork triggers a read-only cache token

GitHub Actions hands fork triggers a read-only cache token

The cache is the part of CI that nobody threat-models until it bites. It is fast, convenient, treated like read-mostly state, and it is one of the few surfaces in a pipeline that an attacker can write to without ever cloning your repository. (You did threat-model the cache, didn't you?) On 2026-06-26 GitHub published a Changelog entry, "Read-only Actions cache for untrusted triggers", that reframes cache writes as a permission, and the new default is no.

Cache as a write target

Think about what a cache entry on the default branch actually is. A blob, stored by GitHub, keyed by something your workflow chose, that the next job on the default branch will silently extract before it runs. If an event that anyone on the internet can fire is allowed to write that blob, your protected branch is now executing code shaped by a stranger. Signed-commit policy does not catch this. Required reviews do not catch this. The artefact never appears in your repo. It just shows up in ~/.cache two minutes into the next build.

What flipped on June 26

GitHub frames the change as applying least privilege to the Actions cache. In operational terms: when both conditions hold, the workflow's cache token is read-only.

  1. The triggering event is untrusted, defined as an event that someone other than a repository collaborator can fire.
  2. The workflow's execution context and cache scope come from the shared default-branch SHA.

GitHub lists the untrusted triggers explicitly: pull_request_target, issue_comment, and fork pull-request workflow_run cascades. These are the same triggers that have been the connective tissue of "pwn request" exploits for years, because they let a fork's payload run with the base repo's identity. Read-only is the floor they now sit on.

The list that stays read-write is also explicit: push, schedule, workflow_dispatch, repository_dispatch, delete, registry_package, and page_build. These come from a principal already inside the trust boundary, so they continue to write to the default-branch cache. Non-default-branch scopes such as pull_request and release keep read-write too, because their cache scope is one an attacker cannot use to influence the default branch.

No opt-in. No setting to toggle. The change shows up the next time the trigger fires.

What you have to wire up

The case that breaks for most teams is the comment-triggered or fork-triggered workflow that used to populate the cache as a side effect of running. That side effect is gone. GitHub's guidance is to move cache saves into a workflow that itself runs on a read-write trigger, typically push, and let the untrusted workflow read what the trusted one wrote.

The minimum split looks roughly like this:

# .github/workflows/cache-warm.yml
on:
  push:
    branches: [main]
jobs:
  warm:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@<full-40-char-sha>
      - uses: actions/cache/save@<full-40-char-sha>
        with:
          key: deps-${{ hashFiles('package-lock.json') }}
          path: node_modules

The PR-target workflow then calls actions/cache/restore@... instead of actions/cache@.... Write on the trusted side of the fence. Read on the untrusted side. Nothing the fork payload does should determine what ends up in next week's default-branch build.

Where this lands across CI platforms

The cache-as-trust-boundary problem is not unique to GitHub. Other platforms handle it with varying explicitness.

  • GitHub Actions is now the only one shipping this split as a platform default. If cache poisoning from fork PRs is on your threat model, this is the closer fit today; the others still expect you to do the work in your config.
  • GitLab CI/CD scopes caches by key and exposes a policy: pull setting that turns a job into a read-only consumer. The trust split is yours to wire into protected-branch and merge-request policies.
  • CircleCI uses content-addressed cache keys. Stopping fork pipelines from writing to default-branch keys is a project configuration step rather than a default.
  • Bitbucket Pipelines scopes caches per repository, with no built-in untrusted-trigger isolation. Teams who care usually split untrusted code into a separate pipeline.
  • Jenkins treats cache as an operator concern. Whether agents share a volume across fork builds depends on your agent strategy, not a Jenkins primitive.
  • Buddy keeps a per-pipeline filesystem cache that does not cross pipelines. One workable shape is to put the cache-warm step in a push-triggered pipeline and have the PR pipeline mount that cache read-only. The concrete reason to reach for this is that the pipeline boundary doubles as the trust boundary; the downside is you are still writing that policy yourself, which is precisely where GitHub Actions has beaten the field.

The residual

Read-only is a floor, not a ceiling. Cache pollution inside trusted triggers is still possible. Keys are still attacker-influenceable through file contents the build itself produces. A workflow that restores a cache it does not validate runs whatever happens to be in the blob. Least privilege on the token is the easy half. Verifying what comes back out is still on you.

Trust the cache only as far as the principal who wrote it.

Source: GitHub Changelog (github.blog)

Related
Security & supply chain

GitHub Actions hands platform teams a workflow-trigger allow list

GitHub Actions is rolling out workflow execution protections in public preview at the enterprise, organization, and repository levels, letting administrators define who and what can trigger workflows. It's the platform-owned trigger gate the CI/CD industry has been quietly working toward for years.

June 18, 2026
Security

actions/checkout v7 refuses fork PR code in pull_request_target

GitHub shipped actions/checkout v7, which fails by default when a workflow triggered by pull_request_target or workflow_run tries to fetch the head of a fork's pull request. Same-repo PRs and the standard pull_request event are unaffected; a deliberately conspicuous opt-out exists for teams who really mean it.

June 18, 2026
Security & supply chain

Pinning every CI action to a commit SHA is becoming the new minimum

A new write-up from the Cilium maintainers lays out a concrete playbook for locking down CI/CD dependencies, full-SHA pinning for every action, digest-pinned containers, vendored Go modules, and Renovate with a release-age cooldown. The pattern matters even if you do not ship eBPF for a living.

June 16, 2026

Turn this into your pipeline. Build it on Buddy.

Start free