A release job fails its tests. The publishing step never runs. Malicious packages still reach npm. That happened during the May 2026 TanStack compromise, according to the project’s incident postmortem.

One year after Shai-Hulud’s first wave, that failure mode deserves more attention than the malware’s name. Removing a reusable publishing secret is valuable. Protecting the environment allowed to obtain its replacement is the next job.

TL;DR

  • The 2025 waves spread through malicious packages and stolen credentials; later campaigns also abused CI publishing identities.
  • Treat pull-request execution, shared caches, and package publication as separate security boundaries.
  • Keep OIDC, but restrict the jobs and inputs that can reach publishing authority.
  • Require independent package approval where practical, and correlate registry releases with the CI runs that supposedly produced them.

A Year of Reused Access

Socket’s September 18 anniversary review follows the name from the original npm worm through subsequent campaigns. The useful history is the sequence of access opportunities:

PeriodDocumented behaviorDefensive implication
September 2025GitHub reported compromised maintainer accounts and malicious post-install scripts. Wiz described stolen npm tokens being used to infect additional packages.A dependency installation can become the starting point for another publisher’s compromise.
November 2025Wiz documented a preinstall payload using setup_bun.js and bun_environment.js, with stolen data appearing in other victims’ GitHub repositories.Check execution and credential exposure across accounts; inspecting only your own repositories is insufficient.
December 2025Wiz traced continued infections to overlooked distribution paths, including private registries and IDE extensions.Registry cleanup does not establish that downstream copies are clean.
May 2026TanStack’s hardening follow-up describes a poisoned CI cache reaching a release environment that used short-lived publishing credentials.Review everything that can supply executable content to the publishing environment.

Sources: GitHub’s September response, Wiz’s initial analysis, November investigation, December follow-up, and TanStack’s hardening report.

Keep attribution separate from that timeline. Socket distinguishes TeamPCP’s later use of the name and publication of code from the September and November 2025 attacks, whose authors it says were never definitively attributed to that group. Wiz’s November analysis also explicitly left attribution unresolved. A shared name is useful for finding reports; it cannot establish a common operator.

The operational thread is easier to establish: code execution exposes authority, and that authority distributes more code. Defenders need an inventory of both sides of that relationship.

TanStack: The Release Step Was Optional

TanStack reported 84 malicious versions across 42 packages on May 11, 2026. Its postmortem describes this chain:

  1. A fork pull request triggered a pull_request_target workflow that executed contributor-controlled code.
  2. That execution poisoned a cache under the base repository’s main-branch scope.
  3. Later release runs restored the poisoned cache.
  4. Malicious code accessed OIDC publishing authority from the runner and submitted packages directly to the registry during testing or cleanup.

The project reported runtime extraction of an OIDC token and said no stored npm publishing tokens were stolen. Its defined publication step was skipped after tests failed. The registry accepted requests authenticated through the trusted-publisher relationship. TanStack postmortem

This makes workflow review a data-flow problem. Reading the final publish command cannot tell you whether another process on the runner can exercise the same authority. Follow the cache, downloaded artifacts, setup scripts, and test commands that run before it.

TanStack subsequently reported disabling release caching, clearing caches, pinning actions to commit hashes, and removing pull_request_target from its CI. These were changes to the execution environment and its inputs. TanStack hardening report

What the Controls Actually Establish

OpenID Connect, or OIDC, lets a supported CI workflow obtain short-lived credentials through an identity relationship configured with npm. It reduces reliance on reusable secrets. The remaining question is what code can act inside that authorized workflow. npm documents repository and workflow bindings, optional environment restrictions, and publishing permissions in its trusted-publisher configuration.

Provenance answers another question: where did this package come from? npm explicitly warns that provenance does not guarantee the absence of malicious code. Use the attestation to inspect source and build context; a badge alone cannot decide whether to install the result. npm provenance limitations

A lockfile serves a different purpose again. npm ci follows the recorded dependency tree and rejects a mismatch with package.json. That helps reproducibility, but a reviewed dependency update can still select a malicious release. Installation scripts are controlled separately; npm ci --ignore-scripts suppresses package scripts, while explicitly invoked commands such as npm test still run their intended script. npm ci documentation

For an application team, the practical question is which installation and execution paths its policy covers. Disabling hooks may require exceptions for legitimate build tooling. Those exceptions should have an owner and a reason. Importing a malicious dependency or running its executable later still requires an execution boundary.

Separate Contribution, Build, and Publication

Start with external contributions. GitHub warns that privileged triggers such as pull_request_target and workflow_run can expose repository authority when combined with untrusted code. It also warns about artifacts passed between workflows. Changing the trigger name without reviewing the transferred content leaves the trust decision unresolved. GitHub secure use reference

For a maintainer, the review should produce a small map: which jobs execute contributor code, which can write caches, which restore those caches, and which can obtain publishing credentials. Include reusable workflows and cleanup steps. Give each path an explicit owner.

Use that map to make three decisions:

  • Contribution jobs: run external code without release credentials or a writable path into release inputs. Verify this with a harmless test pull request in an owned repository.
  • Build jobs: record the source revision and artifact digest. Document every input shared with less-trusted jobs. If a cache’s writers cannot be established, keep it out of the release path until they can.
  • Publication jobs: use a fresh, narrowly scoped environment. Avoid executing build or dependency code there merely for convenience. If publication consumes an artifact, verify its producing run and expected revision before accepting it.

These are design recommendations, not a claim that a fresh runner makes an arbitrary artifact safe. A digest establishes which bytes arrived; the trustworthiness of the producer still needs a decision.

Put Approval Outside the Compromised Job

npm’s staged publishing provides a useful additional boundary. CI submits a candidate with npm stage publish; a maintainer reviews it and approves publication with two-factor authentication. The current documentation requires npm CLI 11.15.0 or later and Node 22.14.0 or higher. npm staged publishing

Configure the trusted publisher to allow staging only, and remove direct publishing permission. npm also recommends disallowing traditional token-based publishing when using trusted publishers. Leaving another direct publication route open undermines the approval requirement. npm trusted-publisher permissions

The review should compare the actual package contents with the intended release: lifecycle hooks, dependency sources, generated JavaScript, included binaries, and unexpected files. Assign a backup approver so an urgent release does not become an argument for permanently bypassing the control.

This adds latency and work. It also creates a decision outside the runner that assembled the candidate. Its value depends on what the reviewer checks and whether their own account and workstation remain trustworthy.

Detect Publication That Does Not Fit the Run

Use the following as investigation hypotheses, not universal signatures. They require registry release information alongside CI metadata and, where available, runner process and network telemetry.

SignalEvidence to collectInvestigation question
Package appears after a failed run or skipped publication stepVersion timestamp, provenance, run ID, attempt number, job and step conclusionsWhich process actually submitted the package?
A brief external PR precedes a release that restores shared stateHistorical workflow logs, checkout revision, cache key and scope, save/restore eventsCould the earlier run change bytes later executed by the release?
Dependency setup contacts an unexpected serviceProcess ancestry, command line, destination, job identityIs this expected installation behavior or credential collection and upload?
New lifecycle hooks or Git dependencies appear in a releasePrevious and candidate package manifests and tarballsDoes the reviewed change explain the new execution path?

Build the first correlation before collecting hundreds of malware filenames. Account for legitimate manual releases and reruns, then alert on cases outside those documented paths. A useful alert should identify the package version and associated run, or state that no expected run was found.

Preserve historical logs promptly. The current PR diff and current workflow definition may differ from what an earlier run executed. Investigators need the revision and event context from that run.

Respond Beyond the Dependency Update

An affected version in a lockfile is a lead. Establish which machines downloaded it, which executed it, and what credentials those processes could access. Include developer workstations, CI workers, internal mirrors, and packaged development tools.

Wiz’s December investigation found that private registries and IDE extensions prolonged exposure after the initial npm response. That is a reason to investigate redistributed copies and installation history when scoping an incident. Wiz’s follow-up

For responders, a practical sequence is:

  1. Suspend affected release automation and isolate suspected execution environments. Preserve volatile evidence where feasible without delaying containment.
  2. Retain workflow logs, artifact digests, package versions, cache metadata, and relevant endpoint telemetry in restricted evidence storage.
  3. Revoke exposed credentials from a clean environment. Review federation bindings and downstream access as well as stored secrets; token expiry does not remove access already established elsewhere.
  4. Rebuild affected workers and workstations as warranted by execution evidence. Remove contaminated cached content after preserving what the investigation needs.
  5. Restore publication after a controlled test demonstrates that an external contribution cannot alter release inputs or exercise publishing authority.

Make that final test part of release engineering’s acceptance criteria. The team should be able to show who supplies every executable input and who approves the package that leaves the pipeline. That is a more useful anniversary milestone than another completed token rotation.

Sources