security: harden publish pipeline + CI against supply-chain injection#25
Merged
Conversation
Addresses Sapper M1 findings H1/H2 (field report: reports/fs-packages/field/2026-04-13-sapper-m1-first-security-assessment.md). ## publish.yml — split build from publish (H2) Previously a single job held both `contents: write` and `id-token: write` while running `npm ci` + `npm run build`. A compromised dev dependency (postinstall or build-time code) could exploit the OIDC token to publish malicious packages with legitimate sigstore provenance — blast radius is every consuming territory. Now split into: - `build` (contents: read, no id-token) — installs deps, builds, uploads dist artifacts. - `publish` (needs: build, contents: write, id-token: write) — downloads artifacts, reinstalls minimal deps, runs `changeset publish`. Both jobs install with `--ignore-scripts` to eliminate the postinstall vector at its root. A malicious build-time dep can still poison the build job's output, but it can no longer mint an OIDC token to publish. ## ci.yml — add --ignore-scripts (defense-in-depth) Same postinstall hardening applied to CI. No id-token exposure here, but a malicious postinstall could still exfiltrate `GITHUB_TOKEN` or other runner context via logs. Pure TypeScript/Vue deps — no legitimate postinstall needs exist in this dependency tree. ## CLAUDE.md — fix package count drift Packages table said 9, actual count is 10 (fs-router was added without updating the doc). Cross-agent note from the Sapper mission. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Deploying fs-packages with
|
| Latest commit: |
1db5b18
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://55a154c7.fs-packages.pages.dev |
| Branch Preview URL: | https://security-harden-publish-work.fs-packages.pages.dev |
oxfmt expects the description column width to match the widest row. Adding fs-router (longer description) requires re-padding the earlier rows. Pure whitespace change; no content difference. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Addresses the two High-severity findings from the first Sapper security assessment on this repo (field report — lives in the war-room meta-repo, not this one).
Problem:
publish.ymlrunsnpm ci+npm run build+changeset publishin a single job holding bothcontents: writeandid-token: write. Any dev-dependency postinstall or build-time code can exploit the OIDC token to publish malicious packages with valid sigstore provenance. Blast radius: every consuming territory (kendo, BIO, ublgenie, entreezuil, the-laboratory).Changes
publish.yml— split build from publishbuildjob —contents: read, noid-token. Installs deps, builds, uploadspackages/*/dist/as an artifact.publishjob —needs: build,contents: write+id-token: write. Downloads dist artifacts, reinstalls minimal deps, runschangeset publish.Job separation means build-time arbitrary code (compromised tsdown plugin, vite plugin, etc.) cannot mint an OIDC token to publish.
Both jobs — `npm ci --ignore-scripts`
Eliminates the postinstall vector entirely. No dev dep in this tree (pure TypeScript/Vue) has a legitimate postinstall requirement.
`ci.yml` — same flag (defense-in-depth)
CI has no `id-token`, but a malicious postinstall could still exfiltrate `GITHUB_TOKEN` or runner context via logs. Same one-line hardening.
Bonus: `CLAUDE.md` fix
Packages table said 9, actual is 10 — `fs-router` was added without updating the doc. Surfaced by the Sapper as a cross-agent note.
Residual risk (not fixed by this PR)
A compromised build-time dev dep can still alter the uploaded `dist/` artifact before the publish job consumes it — the split removes the OIDC minting path but not the build poisoning path. Harder fix: dependency pinning review, signed lockfile, or a trusted build environment. Out of scope here.
The other H1 finding (no branch protection + 6 admins) is handled outside this PR — branch protection is a repo settings change, not code. Planned for after this merges so the CI `check` job can be required as a status check.
Test plan
🤖 Generated with Claude Code