| date | 2026-05-12 |
|---|---|
| scope | npm, PyPI, VS Code Marketplace, and Open VSX publishing surface; GitHub Actions CI/CD; lockfile and lifecycle-script handling. |
| method | Threat-model review against the TanStack / mini-shai-hulud npm worm (2026-05-11), followed by gap fixes in release.yml, ci.yml, and SECURITY.md. |
| title | Supply-Chain Hardening — mini-shai-hulud / TanStack Class |
| summary | Confirms mdsmith is not vulnerable to the direct TanStack attack chain (pull_request_target pwn, fork-network orphan-commit OIDC abuse, runtime memory extraction of publish tokens). Adds defense-in-depth controls: concurrency group, repository-and- environment guards on publishing jobs, `--ignore-scripts` on every `bun install`, a CI guard that fails any future PR introducing npm lifecycle hooks, and centralizes the release pipeline documentation. |
The steady-state release pipeline and the full list
of supply-chain controls live in
docs/development/release.md.
This note is a point-in-time threat model: what the
attack was, which surfaces it touched in mdsmith, and
which gaps the 2026-05-12 commit closed.
On 2026-05-11 between 19:20 and 19:26 UTC an attacker
published 84 malicious versions across 42
@tanstack/* npm packages. The same worm family also
hit @mistralai/*, @uipath/*, @squawk/*, and
others. The Aikido write-up and the
TanStack postmortem (linked from
tanstack/router#7383) describe a
three-stage chain:
pull_request_target"Pwn Request" — a fork PR triggered a workflow that checked out and executed the fork's code with access to base-repo secrets.- GitHub Actions cache poisoning across the fork↔base trust boundary — the malicious fork wrote a poisoned pnpm store into the shared cache; the legitimate release workflow restored that cache.
- Runtime OIDC token extraction — the malware
located the
Runner.Workerprocess, read/proc/<pid>/mapsand/proc/<pid>/mem, and pulled the GitHub Actions OIDC token from memory. Because TanStack's npm Trusted Publisher was scoped at the repository level (not pinned to a protected branch / ref / workflow file / environment), the stolen token minted a valid short-lived npm publish credential. The worm published validly-attested SLSA Build Level 3 provenance for malicious packages.
A parallel vector used by other mini-shai-hulud
waves is the classic preinstall / postinstall
lifecycle script that scans the install host for npm
tokens, GitHub PATs, ~/.npmrc, ~/.gitconfig, SSH
keys, and cloud credentials. The worm then
republishes infected versions of every package the
stolen npm token can write.
| Surface | Path | Verdict |
|---|---|---|
| npm root package | npm/mdsmith/ |
No lifecycle scripts; files: allowlist; npm shim uses frozen platform-package map. |
| npm platform packages | built by mdsmith-release |
No lifecycle scripts (binary-only). |
| PyPI wheel | python/ |
Wheel-only (no sdist); no install-time code. |
| VS Code extension | editors/vscode/ |
vsce package --no-dependencies strips deps from the published .vsix. |
| Claude Code plugin | editors/claude-code/ |
Marketplace metadata only; no executable payload. |
| Release workflow | .github/workflows/release.yml |
OIDC trusted publishing, pinned actions, cache: false, persist-credentials: false. |
| CI workflow | .github/workflows/ci.yml |
pull_request only (no pull_request_target); zizmor self-audit; codecov OIDC fork-gated. |
| Demo / merge-queue / record workflows | .github/workflows/ |
No untrusted-fork execution paths. |
Reviewing the exact TanStack attack chain step by step:
- No
pull_request_targettrigger anywhere —grep -rn 'pull_request_target' .github/returns no hits. Fork PRs cannot execute privileged workflows. - No
workflow_runtrigger — eliminates the secondary chained-workflow vector other shai- hulud waves used. - No
actions/github-scriptstep — no surface for inline JavaScript that runs with theGITHUB_TOKENof a fork PR. - No
github:owner/repo#shagit URL dependencies —grep -rn '"github:' npm/ editors/is empty, so the fork-network orphan- commit specifier vector cannot reach mdsmith's installable surface. - GitHub Actions tool cache disabled in the
release workflow —
setup-gois invoked withcache: falseandsetup-bunwithno-cache: true. The release path cannot restore a poisoned cache. - No
preinstall/postinstall/installlifecycle hooks in any published manifest. The npm shim deliberately resolves the platform binary at runtime, not at install time. Thenpm-lifecycle-guardCI job now enforces this in CI rather than relying on convention. - npm publishing uses Trusted Publishing
(OIDC) — no long-lived
NODE_AUTH_TOKENfor a worm to harvest from~/.npmrcor env vars. - PyPI publishing uses Trusted Publishing (OIDC) — same property.
- All third-party GitHub Actions are pinned to
commit SHAs — a tag move on
softprops/action-gh-release,actions/checkout,pypa/gh-action-pypi-publish, orcodecov/codecov-actioncannot silently pull a malicious version. - zizmor runs in CI and fails the job on any finding — the same scanner that flagged the TanStack cache-poisoning surface.
The above leaves four residual concerns. The
2026-05-12 commit closes them and centralizes the
ongoing posture in
docs/development/release.md.
The npm Trusted Publisher was configured to require
repo=jeduden/mdsmith and workflow=release.yml.
That is strictly better than the TanStack baseline.
A successful release.yml run on any ref could
still mint a publish token. Adding
environment: release to every job that holds
id-token: write (or a long-lived publisher PAT)
introduces an environment claim in the OIDC token.
With the environment claim required at the npm /
PyPI side, an attacker who somehow runs release.yml
outside the release environment cannot mint a valid
publish token. The
release pipeline operational checklist
lists the corresponding npmjs.com, pypi.org, and
GitHub UI steps.
A second git push --tags while a release was in
flight previously ran the publish jobs in parallel
against the same registry record. The new
concurrency: { group: release, cancel-in-progress: false }
at the workflow level serializes every release run
— tag-agnostic, so different tags pushed back-to-back
queue rather than overlap. The first publish
completes; the second runs after.
The VS Code extension's dev dependency tree
(@vscode/vsce, typescript, @types/*,
vscode-languageclient) is pure-JS today. A future
lockfile update or registry compromise could
introduce a postinstall hook. Every
bun install --frozen-lockfile invocation now also
passes --ignore-scripts, neutralizing the install-
time code execution path the shai-hulud worm relies
on. If a future dep genuinely needs a lifecycle
script, the requirement should be documented here
before the flag is relaxed for that step.
ci.yml's new npm-lifecycle-guard job rejects any
PR that adds an install-, uninstall-, prepare-, pack-,
or publish-time lifecycle hook to
npm/mdsmith/package.json or
editors/vscode/package.json. The full banned set
lives in the banned= line of
.github/workflows/ci.yml (15 hooks at time of
writing: preinstall, install, postinstall,
preuninstall, uninstall, postuninstall,
preprepare, prepare, postprepare, prepublish,
prepublishOnly, prepack, postpack, publish,
postpublish). The guard is intentionally noisy:
changing it requires updating this security note.
- Long-lived publisher PATs.
VSCE_PATandOVSX_PATare gated by thereleaseGitHub environment so a workflow run outside an approved release cannot read them.MERGE_QUEUE_TOKEN, consumed bymerge-queue.ymlon PR-label events, is intentionally not env-scoped (the merge queue runs on every PR-merge cycle, not tag pushes) — its blast radius is branch-protection bypass onmain. The scheduledsecret-rotation-reminder.ymlworkflow opens a labelled GitHub issue 30 days before each tracked secret'slastRotated + periodDaysdue date, andrecord-secret-rotation.ymlis the correspondingworkflow_dispatchthat records the rotation by opening a PR. Per-secret files underdocs/development/secret-rotations/hold the rotation procedure; see secret-rotations.md for the catalog and overall workflow. codecov-actionwithid-token: writeruns on PRs from same-repo branches. The OIDC token's audience iscodecov, so it cannot be replayed against npm or PyPI. The upload step isif-gated to skip fork PRs.- Lockfiles are not verified against a content-
addressed registry.
bun audit signatureswould catch a registry-side tampering; future work.
- Aikido: Mini Shai-Hulud Is Back
- TanStack postmortem linked from tanstack/router#7383
- GitHub Security Advisory GHSA-g7cv-rxg3-hmpx
- npm Trusted Publishers docs
- PyPI Trusted Publishers docs
- Datadog Security Labs: Shai-Hulud 2.0 npm worm analysis
- Snyk: NPM Security Best Practices After the 2025 Shai-Hulud Attack