Skip to content

When pnpm/Yarn support lands, do NOT enable pnpm-store / yarn cache (Mini Shai-Hulud lesson) #205

@TomHennen

Description

@TomHennen

Goal

When wrangle adds pnpm / Yarn support in a future v0.2+ iteration (currently noted in build/actions/npm/SPEC.md "v0.1 limitations"), do NOT enable setup-node-style package-manager caching for those flows.

Why

The May 2026 Mini Shai-Hulud compromise of TanStack/router exploited pnpm-store cache poisoning. The mechanism:

  • actions/setup-node (and pnpm/action-setup) cache pnpm-store — a content-addressed store of extracted modules under <integrity-hash>/<file> paths.
  • pnpm install hardlinks files from the store into node_modules/. pnpm does not re-verify that the file at path <hash>/<file> actually hashes to <hash> at install time. The integrity hash is treated as a label, not an enforced guard.
  • An attacker with code execution in any workflow on the repo (e.g., via the pull_request_target "pwn request" pattern) can write malicious content to those store paths. The poisoned store gets cached by GitHub Actions. Subsequent legitimate workflow runs restore the cache, pnpm hardlinks the malicious bytes into node_modules/, the build runs with malicious tools, output is malicious.
  • The build is otherwise legitimate (legitimate source, legitimate commit, legitimate signer) — so any provenance attestation (L2 npm-CLI or L3 slsa-github-generator) faithfully signs the malicious output.

The npm equivalent (cache: 'npm', caches ~/.npm tarballs) is NOT vulnerable to this same pattern, because npm ci re-validates each cached tarball's integrity field against package-lock.json on every install. A cache entry whose tarball doesn't hash to the lockfile's pinned integrity is rejected before extraction. This is the trust property pnpm-store lacks.

What this means concretely

When pnpm / Yarn support lands:

  • Do NOT set cache: 'pnpm' or cache: 'yarn' in actions/setup-node invocations from wrangle's build actions.
  • Do NOT cache ~/.local/share/pnpm/store / ~/.yarn/cache via actions/cache directly.
  • Document the choice in build/actions/npm/SPEC.md (or the renamed file when it covers all three package managers) with a "Threat model: caching" section pointing at this issue + the TanStack postmortem.
  • Add a bats structural test that asserts no pnpm/yarn cache config exists in build/actions/*/action.yml.

Where this is allowed today

The npm flow on main has cache: 'npm' in build/actions/npm/action.yml. That's safe to keep — see "Why" above. There's a comment block in action.yml already explaining the threat model; expand it when pnpm support lands to spell out the asymmetry.

Out of scope

  • Disabling caching entirely. Caching is fine where it's safe (npm). The constraint is package-manager-specific.
  • Adding extraction-time re-verification to pnpm-store. That's pnpm's job, not wrangle's.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions