Skip to content

security: harden publish pipeline + CI against supply-chain injection#25

Merged
Goosterhof merged 2 commits into
mainfrom
security/harden-publish-workflow
Apr 13, 2026
Merged

security: harden publish pipeline + CI against supply-chain injection#25
Goosterhof merged 2 commits into
mainfrom
security/harden-publish-workflow

Conversation

@Goosterhof
Copy link
Copy Markdown
Contributor

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.yml runs npm ci + npm run build + changeset publish in a single job holding both contents: write and id-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 publish

  • build jobcontents: read, no id-token. Installs deps, builds, uploads packages/*/dist/ as an artifact.
  • publish jobneeds: build, contents: write + id-token: write. Downloads dist artifacts, reinstalls minimal deps, runs changeset 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

  • CI `check` job passes on this PR (verifies `--ignore-scripts` doesn't break the dev-dep install)
  • Verify publish workflow syntactically (no trigger until a `package.json` change lands on main)
  • First release after merge: confirm artifact handoff works end-to-end (build job uploads, publish job downloads, `changeset publish` finds all `dist/` directories)
  • Rollback plan: revert the PR if the next release fails; the prior workflow shape is one commit away

🤖 Generated with Claude Code

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>
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Apr 13, 2026

Deploying fs-packages with  Cloudflare Pages  Cloudflare Pages

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

View logs

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>
@Goosterhof Goosterhof merged commit 07f691a into main Apr 13, 2026
2 checks passed
@Goosterhof Goosterhof deleted the security/harden-publish-workflow branch April 13, 2026 10:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant