Skip to content

Commit df2a59a

Browse files
byshingclaude
andcommitted
Harden CI against supply chain attacks
- Pin all GitHub Actions to immutable commit SHAs (prevents tag mutation attacks) - Set permissions: read-all at workflow level; grant contents: write only to the host job that creates releases (principle of least privilege) - Isolate CI cache from release cache using branch-prefixed keys to prevent cache poisoning across fork/PR trust boundaries Mitigates the same attack chain used in the TanStack npm compromise (2026-05-11): pull_request_target pwn request + cache poisoning + OIDC token extraction. Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
1 parent ad826a6 commit df2a59a

2 files changed

Lines changed: 27 additions & 24 deletions

File tree

.github/workflows/ci.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
name: CI
22

3+
permissions: read-all
4+
35
on:
46
push:
57
branches: [master]
@@ -9,14 +11,14 @@ jobs:
911
test:
1012
runs-on: ubuntu-latest
1113
steps:
12-
- uses: actions/checkout@v6
14+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
1315
- run: sudo apt-get install -y libdbus-1-dev
14-
- uses: actions/cache@v4
16+
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
1517
with:
1618
path: |
1719
~/.cargo/registry
1820
~/.cargo/git
1921
target
20-
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
21-
restore-keys: ${{ runner.os }}-cargo-
22+
key: ${{ github.ref == 'refs/heads/master' && 'master' || 'pr' }}-${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
23+
restore-keys: ${{ github.ref == 'refs/heads/master' && 'master' || 'pr' }}-${{ runner.os }}-cargo-
2224
- run: cargo test

.github/workflows/release.yml

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,7 @@
1414
# title/body based on your changelogs.
1515

1616
name: Release
17-
permissions:
18-
"contents": "write"
17+
permissions: read-all
1918

2019
# This task will run whenever you push a git tag that looks like a version
2120
# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc.
@@ -56,7 +55,7 @@ jobs:
5655
env:
5756
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
5857
steps:
59-
- uses: actions/checkout@v6
58+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
6059
with:
6160
persist-credentials: false
6261
submodules: recursive
@@ -66,7 +65,7 @@ jobs:
6665
shell: bash
6766
run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.31.0/cargo-dist-installer.sh | sh"
6867
- name: Cache dist
69-
uses: actions/upload-artifact@v6
68+
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
7069
with:
7170
name: cargo-dist-cache
7271
path: ~/.cargo/bin/dist
@@ -82,7 +81,7 @@ jobs:
8281
cat plan-dist-manifest.json
8382
echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
8483
- name: "Upload dist-manifest.json"
85-
uses: actions/upload-artifact@v6
84+
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
8685
with:
8786
name: artifacts-plan-dist-manifest
8887
path: plan-dist-manifest.json
@@ -127,7 +126,7 @@ jobs:
127126
- name: enable windows longpaths
128127
run: |
129128
git config --global core.longpaths true
130-
- uses: actions/checkout@v6
129+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
131130
with:
132131
persist-credentials: false
133132
submodules: recursive
@@ -138,15 +137,15 @@ jobs:
138137
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
139138
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
140139
fi
141-
- uses: swatinem/rust-cache@v2
140+
- uses: swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
142141
with:
143142
key: ${{ join(matrix.targets, '-') }}
144143
cache-provider: ${{ matrix.cache_provider }}
145144
- name: Install dist
146145
run: ${{ matrix.install_dist.run }}
147146
# Get the dist-manifest
148147
- name: Fetch local artifacts
149-
uses: actions/download-artifact@v7
148+
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
150149
with:
151150
pattern: artifacts-*
152151
path: target/distrib/
@@ -175,7 +174,7 @@ jobs:
175174
--team-id "$APPLE_NOTARIZE_TEAM_ID" \
176175
--wait
177176
- name: Attest
178-
uses: actions/attest-build-provenance@v3
177+
uses: actions/attest-build-provenance@977bb373ede98d70efdf65b84cb5f73e068dcc2a # v3
179178
with:
180179
subject-path: "target/distrib/*${{ join(matrix.targets, ', ') }}*"
181180
- id: cargo-dist
@@ -192,7 +191,7 @@ jobs:
192191
193192
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
194193
- name: "Upload artifacts"
195-
uses: actions/upload-artifact@v6
194+
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
196195
with:
197196
name: artifacts-build-local-${{ join(matrix.targets, '_') }}
198197
path: |
@@ -209,19 +208,19 @@ jobs:
209208
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
210209
BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
211210
steps:
212-
- uses: actions/checkout@v6
211+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
213212
with:
214213
persist-credentials: false
215214
submodules: recursive
216215
- name: Install cached dist
217-
uses: actions/download-artifact@v7
216+
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
218217
with:
219218
name: cargo-dist-cache
220219
path: ~/.cargo/bin/
221220
- run: chmod +x ~/.cargo/bin/dist
222221
# Get all the local artifacts for the global tasks to use (for e.g. checksums)
223222
- name: Fetch local artifacts
224-
uses: actions/download-artifact@v7
223+
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
225224
with:
226225
pattern: artifacts-*
227226
path: target/distrib/
@@ -239,7 +238,7 @@ jobs:
239238
240239
cp dist-manifest.json "$BUILD_MANIFEST_NAME"
241240
- name: "Upload artifacts"
242-
uses: actions/upload-artifact@v6
241+
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
243242
with:
244243
name: artifacts-build-global
245244
path: |
@@ -253,25 +252,27 @@ jobs:
253252
- build-global-artifacts
254253
# Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine)
255254
if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}
255+
permissions:
256+
contents: write
256257
env:
257258
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
258259
runs-on: "ubuntu-22.04"
259260
outputs:
260261
val: ${{ steps.host.outputs.manifest }}
261262
steps:
262-
- uses: actions/checkout@v6
263+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
263264
with:
264265
persist-credentials: false
265266
submodules: recursive
266267
- name: Install cached dist
267-
uses: actions/download-artifact@v7
268+
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
268269
with:
269270
name: cargo-dist-cache
270271
path: ~/.cargo/bin/
271272
- run: chmod +x ~/.cargo/bin/dist
272273
# Fetch artifacts from scratch-storage
273274
- name: Fetch artifacts
274-
uses: actions/download-artifact@v7
275+
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
275276
with:
276277
pattern: artifacts-*
277278
path: target/distrib/
@@ -284,14 +285,14 @@ jobs:
284285
cat dist-manifest.json
285286
echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
286287
- name: "Upload dist-manifest.json"
287-
uses: actions/upload-artifact@v6
288+
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
288289
with:
289290
# Overwrite the previous copy
290291
name: artifacts-dist-manifest
291292
path: dist-manifest.json
292293
# Create a GitHub Release while uploading all files to it
293294
- name: "Download GitHub Artifacts"
294-
uses: actions/download-artifact@v7
295+
uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
295296
with:
296297
pattern: artifacts-*
297298
path: artifacts
@@ -324,7 +325,7 @@ jobs:
324325
env:
325326
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
326327
steps:
327-
- uses: actions/checkout@v6
328+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
328329
with:
329330
persist-credentials: false
330331
submodules: recursive

0 commit comments

Comments
 (0)