From b223a1433f17f5c0ceb4503d0cf914b155fc7b6a Mon Sep 17 00:00:00 2001 From: Stephen Ayotte Date: Wed, 28 Oct 2026 13:50:45 -0400 Subject: [PATCH 1/7] Pin actions/checkout to full commit SHAs Git tags are mutable: whoever controls the upstream repository (or an attacker who compromises it) can move `v4` or even `v7.0.1` to point at different code, and every workflow referencing the tag will silently run it on the next trigger. This has happened in the wild (e.g. the tj-actions/changed-files compromise, CVE-2025-30066). Pinning to the full 40-character commit SHA makes the reference immutable. The SHAs below are the commits the existing tags resolve to today, so CI behavior is unchanged; the trailing comment records the exact release for readability and for update tooling such as Dependabot. - actions/checkout@v4 -> 11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - actions/checkout@v7.0.1 -> 3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/docs.yml | 2 +- .github/workflows/main.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f9d7850..8e86160 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -15,7 +15,7 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - uses: cachix/install-nix-action@v27 - uses: actions/configure-pages@v5 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 85b0576..fc94d21 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -7,7 +7,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Check-out Repo - uses: actions/checkout@v7.0.1 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Nix uses: cachix/install-nix-action@31.11.3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 51355f3..c7cfda8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: fetch-depth: 0 - run: git fetch --tags --force origin From ae7642ab76643f43737cf46be8c4313c476b4a59 Mon Sep 17 00:00:00 2001 From: Stephen Ayotte Date: Wed, 28 Oct 2026 13:50:54 -0400 Subject: [PATCH 2/7] Pin cachix/install-nix-action to full commit SHAs Same rationale as the actions/checkout pin: a mutable tag lets the upstream (or anyone who compromises it) change the code CI executes without any change in this repository. install-nix-action is especially sensitive because it runs with the job's token and installs the Nix toolchain that every subsequent build step executes inside. The SHAs are the commits the current tags resolve to, so behavior is unchanged: - cachix/install-nix-action@v27 -> ba0dd844c9180cbf77aa72a116d6fbc515d0e87b # v27 - cachix/install-nix-action@31.11.3 -> 75c75c31ebece974e48ebc36b4ad125bd97bd97b # v31.11.3 (Upstream publishes `v27` only as a major tag with no v27.x.y release tag on that commit, so the comment keeps `v27`. `31.11.3` and `v31.11.3` are both upstream tags on the same commit; the comment uses the v-prefixed form for consistency with the other pins.) Co-Authored-By: Claude Opus 5.5 --- .github/workflows/docs.yml | 2 +- .github/workflows/main.yml | 2 +- .github/workflows/release.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 8e86160..b056d49 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - - uses: cachix/install-nix-action@v27 + - uses: cachix/install-nix-action@ba0dd844c9180cbf77aa72a116d6fbc515d0e87b # v27 - uses: actions/configure-pages@v5 - name: Build Docs diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index fc94d21..1bcda21 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,7 +10,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Setup Nix - uses: cachix/install-nix-action@31.11.3 + uses: cachix/install-nix-action@75c75c31ebece974e48ebc36b4ad125bd97bd97b # v31.11.3 - name: Build & Run Tests run: nix develop -c ./do test --release diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c7cfda8..5051b09 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -17,7 +17,7 @@ jobs: with: fetch-depth: 0 - run: git fetch --tags --force origin - - uses: cachix/install-nix-action@v27 + - uses: cachix/install-nix-action@ba0dd844c9180cbf77aa72a116d6fbc515d0e87b # v27 - run: nix develop -c ./do package - run: nix develop -c ./do release-notes ${GITHUB_REF_NAME#v} From 04ecdfb8735aa1a4adf1e6b19e845bbfcb874595 Mon Sep 17 00:00:00 2001 From: Stephen Ayotte Date: Wed, 28 Oct 2026 13:50:59 -0400 Subject: [PATCH 3/7] Pin GitHub Pages actions to full commit SHAs Same rationale as the other pins. deploy-pages is the most sensitive of the three: its job holds `pages: write` and `id-token: write`, so code substituted behind a moved tag could deface the docs site or mint OIDC tokens for this repository. The SHAs are the commits the current tags resolve to, so behavior is unchanged: - actions/configure-pages@v5 -> 983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0 - actions/upload-pages-artifact@v3 -> 56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 - actions/deploy-pages@v4 -> d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 Note: upload-pages-artifact is a composite action that internally calls actions/upload-artifact by tag, which this repository cannot pin; see the PR description. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/docs.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b056d49..26486f5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -17,12 +17,12 @@ jobs: steps: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 - uses: cachix/install-nix-action@ba0dd844c9180cbf77aa72a116d6fbc515d0e87b # v27 - - uses: actions/configure-pages@v5 + - uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0 - name: Build Docs run: nix develop -c mkdocs build - - uses: actions/upload-pages-artifact@v3 + - uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 with: path: dist/docsite @@ -38,4 +38,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 From 89c481d2227a880fe115c703c3a8d4826b882764 Mon Sep 17 00:00:00 2001 From: Stephen Ayotte Date: Wed, 28 Oct 2026 13:51:08 -0400 Subject: [PATCH 4/7] Pin softprops/action-gh-release to a full commit SHA This is the highest-value pin in the repository. The release workflow runs with `contents: write` and this action is what publishes the .vsix that users install, so a moved `v2` tag would let a third party replace or tamper with release assets or rewrite release notes. Unlike the actions/* pins it is maintained by an individual, which widens the set of accounts whose compromise reaches this repo. The SHA is the commit the current `v2` tag resolves to (a self-contained node20 action with a bundled dist/index.js and no nested `uses:`), so behavior is unchanged: - softprops/action-gh-release@v2 -> 3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5051b09..34a8437 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,7 +35,7 @@ jobs: exit 1 fi - - uses: softprops/action-gh-release@v2 + - uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2 with: body_path: release-notes.md files: dist/*.vsix From 5980a942c295e31ee79967d06a8d5afa1759cd5d Mon Sep 17 00:00:00 2001 From: Stephen Ayotte Date: Wed, 28 Oct 2026 13:51:17 -0400 Subject: [PATCH 5/7] Restrict Build & Test workflow token to contents: read main.yml had no `permissions:` block, so its GITHUB_TOKEN inherits the repository/organization default. For repositories created before February 2023 (and any repo where the setting was never changed) that default is read/write across most scopes, including contents, packages, actions and pull-requests. The job only checks out code, builds, tests and packages; it never writes to the repository or the API. Declaring `contents: read` explicitly makes the workflow least-privilege regardless of the repo setting, so a compromised build dependency (npm packages are installed with lifecycle scripts during `./do test`/`./do package`) cannot use the token to push commits, move tags, or tamper with releases. Fork pull requests already receive a read-only token, so this changes nothing for them; it only narrows `push` runs to what they actually use. This matches the explicit blocks already present in docs.yml and release.yml. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/main.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1bcda21..9a10778 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,6 +2,9 @@ name: Build & Test on: [push, pull_request] +permissions: + contents: read + jobs: build: runs-on: ubuntu-latest From 4721e85a665822e44b1160785f37990d9b163af6 Mon Sep 17 00:00:00 2001 From: Stephen Ayotte Date: Wed, 28 Oct 2026 13:51:27 -0400 Subject: [PATCH 6/7] Don't persist the write-scoped token in the release checkout By default actions/checkout writes the job's GITHUB_TOKEN into .git/config so later steps can run authenticated git commands. In the release job that token has `contents: write`, and the steps that follow run `nix develop -c ./do package`, which executes `npm install` (with package lifecycle scripts) and `npx vsce`. Any compromised package in that dependency tree could read the token from .git/config and push commits, move tags, or alter releases. Nothing after checkout needs the persisted credential: - `git fetch --tags --force origin` reads from a public repository and works anonymously. - `./do package` / `./do release-notes` make no authenticated git or API calls. - softprops/action-gh-release receives the token through its own `token` input (default `github.token`), not from git config. Setting `persist-credentials: false` keeps the token available only to the action that actually needs it. Behavior of the release is otherwise unchanged. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 34a8437..51e7a37 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,6 +16,7 @@ jobs: - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 with: fetch-depth: 0 + persist-credentials: false - run: git fetch --tags --force origin - uses: cachix/install-nix-action@ba0dd844c9180cbf77aa72a116d6fbc515d0e87b # v27 - run: nix develop -c ./do package From 10f072ef963295df22a86e1f8f4ca0a487b6955c Mon Sep 17 00:00:00 2001 From: Stephen Ayotte Date: Wed, 28 Oct 2026 13:51:34 -0400 Subject: [PATCH 7/7] Quote tag-derived argument in release workflow actionlint/shellcheck flags SC2086 on the unquoted ${GITHUB_REF_NAME#v} passed to `./do release-notes`. It is not exploitable today, because the workflow's tag filter (v[0-9]+.[0-9]+.[0-9]+) means the value can only contain digits and dots, but quoting it is the correct form, keeps the workflow lint-clean, and stays safe if the trigger filter is ever loosened. No behavior change for any tag the workflow currently accepts. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 51e7a37..00d54de 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: - run: git fetch --tags --force origin - uses: cachix/install-nix-action@ba0dd844c9180cbf77aa72a116d6fbc515d0e87b # v27 - run: nix develop -c ./do package - - run: nix develop -c ./do release-notes ${GITHUB_REF_NAME#v} + - run: nix develop -c ./do release-notes "${GITHUB_REF_NAME#v}" - name: Read release channel from tag annotation id: channel