You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: src/pentesting-ci-cd/github-security/abusing-github-actions/README.md
+8Lines changed: 8 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -498,6 +498,14 @@ GitHub exposes a cross-workflow cache that is keyed only by the string you suppl
498
498
- Official actions (`setup-node`, `setup-python`, dependency caches, etc.) frequently reuse deterministic keys, so identifying the correct key is trivial once the workflow file is public.
499
499
- Restores are just zstd tarball extractions with no integrity checks, so poisoned caches can overwrite scripts, `package.json`, or other files under the restore path.
500
500
501
+
**Advanced techniques (Angular 2026 case study)**
502
+
503
+
- Cache v2 behaves as if all keys are restore keys: an exact miss can still restore a different entry that shares the same prefix, which enables near-collision pre-seeding attacks.
504
+
- Since **November 20, 2025**, GitHub evicts cache entries immediately once repository cache size exceeds the quota (10 GB by default). Attackers can bloat cache usage with junk, force eviction, and write poisoned entries in the same workflow run.
505
+
- Reusable actions wrapping `actions/setup-node` with `cache-dependency-path` can create hidden trust-boundary overlap, letting an untrusted workflow poison caches later consumed by secret-bearing bot/release workflows.
506
+
- A realistic post-poisoning pivot is stealing a bot PAT and force-pushing approved bot PR heads (if approval-reset rules exempt bot actors), then swapping action SHAs to imposter commits before maintainers merge.
507
+
- Tooling like `Cacheract` automates cache runtime token handling, cache eviction pressure, and poisoned entry replacement, which reduces operational complexity during authorized red-team simulation.
508
+
501
509
**Mitigations**
502
510
503
511
- Use distinct cache key prefixes per trust boundary (e.g., `untrusted-` vs `release-`) and avoid falling back to broad `restore-keys` that allow cross-pollination.
Copy file name to clipboardExpand all lines: src/pentesting-ci-cd/github-security/abusing-github-actions/gh-actions-cache-poisoning.md
+104Lines changed: 104 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -60,10 +60,114 @@ On a cache hit, the restore action will extract the archive as-is. If the cache
60
60
- Look for deterministic cache keys reused across trust boundaries (for example, `pip-${{ hashFiles('poetry.lock') }}`) or permissive `restore-keys`, then save your malicious tarball before the privileged workflow runs.
61
61
- Monitor logs for `Cache saved` entries or add your own cache-save step so the next release job restores the payload and executes the trojanized scripts or binaries.
62
62
63
+
## Newer techniques seen in the Angular (2026) chain
64
+
65
+
-**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.
66
+
-**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.
67
+
-**`setup-node` cache pivots via reusable actions:** Reusable/internal actions that wrap `actions/setup-node` with `cache-dependency-path` can 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).
68
+
-**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.
69
+
70
+
##å Cacheract
71
+
72
+
[`Cacheract`](https://github.com/adnanekhan/cacheract) is a PoC-focused toolkit for GitHub Actions cache poisoning in authorized testing. The practical value is that it automates the fragile parts that are easy to get wrong manually:
73
+
74
+
- Detect and use runtime cache context from the runner (`ACTIONS_RUNTIME_TOKEN` and cache service URL).
75
+
- Enumerate and target candidate cache keys/versions used by downstream workflows.
76
+
- Force eviction by overfilling cache quota (when applicable) and then writing attacker-controlled entries in the same run.
77
+
- Seed poisoned cache content so later workflows restore and execute modified tooling.
78
+
79
+
This is especially useful in Cache v2 environments where timing and key/version behavior matter more than in early cache implementations.
80
+
81
+
## Demo
82
+
83
+
Use this only in repositories you own or are explicitly allowed to test.
84
+
85
+
### 1. Vulnerable workflow (untrusted trigger can save cache)
86
+
87
+
This workflow simulates a `pull_request_target` anti-pattern: it writes cache content from attacker-controlled context and saves it under a deterministic key.
88
+
89
+
```yaml
90
+
name: untrusted-cache-writer
91
+
on:
92
+
pull_request_target:
93
+
types: [opened, synchronize, reopened]
94
+
95
+
permissions:
96
+
contents: read
97
+
98
+
jobs:
99
+
poison:
100
+
runs-on: ubuntu-latest
101
+
steps:
102
+
- uses: actions/checkout@v4
103
+
- name: Build "toolchain" from untrusted context (demo)
### 2. Privileged workflow (restores and executes cached binary/script)
119
+
120
+
This workflow restores the same key and executes `toolchain/bin/build` while holding a dummy secret. If poisoned, execution path is attacker-controlled.
test -f /tmp/cache-poisoning-demo.txt && echo "Poisoning confirmed"
144
+
```
145
+
146
+
### 3. Run the lab
147
+
148
+
- Add a stable `toolchain.lock` file so both workflows resolve the same cache key.
149
+
- Trigger `untrusted-cache-writer` from a test PR.
150
+
- Trigger `privileged-consumer` via `workflow_dispatch`.
151
+
- Confirm `POISONED_BUILD_PATH` appears in logs and `/tmp/cache-poisoning-demo.txt` is created.
152
+
153
+
### 4. What this demonstrates technically
154
+
155
+
- **Cross-workflow cache trust break:** The writer and consumer workflows do not share trust level, but they share cache namespace.
156
+
- **Execution-on-restore risk:** No integrity validation is performed before executing a restored script/binary.
157
+
- **Deterministic key abuse:** If a high-trust job uses predictable keys, a low-trust job can preposition malicious content.
158
+
159
+
### 5. Defensive verification checklist
160
+
161
+
- Split keys by trust boundary (`pr-`, `ci-`, `release-`) and avoid shared prefixes.
162
+
- Disable cache writes in untrusted workflows.
163
+
- Hash/verify restored executable content before running it.
164
+
- Avoid executing tools directly from cache paths.
165
+
63
166
## References
64
167
65
168
- [A Survey of 2024–2025 Open-Source Supply-Chain Compromises and Their Root Causes](https://words.filippo.io/compromise-survey/)
66
169
- [The Monsters in Your Build Cache: GitHub Actions Cache Poisoning](http://adnanthekhan.com/2024/05/06/the-monsters-in-your-build-cache-github-actions-cache-poisoning/)
170
+
- [Turning Almost Nothing into a Supply Chain Compromise of Angular with GitHub Actions Cache Poisoning](https://adnanthekhan.com/posts/angular-compromise-through-dev-infra/)
0 commit comments