PatchDay Alert
Analysis · 8 min read · 1,595 words By analysis-desk

What 14 days of TeamPCP told us about registry defense in 2026

Five compromises across two ecosystems in six weeks, then a 169-package npm wave on May 11. One threat actor, two very different defensive postures. The pattern is the point.

What 14 days of TeamPCP told us about registry defense in 2026

Update: May 13, 2026. This post missed a wave. On May 11, between 19:20 and 19:26 UTC, the same threat actor pushed 84 malicious versions across 42 @tanstack/* packages and continued through 169 npm packages total, hitting Mistral AI, UiPath, OpenSearch, Guardrails AI, and Squawk. The TanStack compromise was assigned CVE-2026-45321, CVSS 9.6. Ashish Kurmi at StepSecurity opened a public issue on the TanStack repo roughly 20 minutes after the first malicious publish, and StepSecurity attributed the wave to TeamPCP based on shared infrastructure carried over from prior campaigns.

Two details reshape the read below. First, the initial-access chain ran further upstream of the registry than anything in the original sequence: a pull_request_target Pwn Request, GitHub Actions cache poisoning across the fork-to-base trust boundary, and OIDC token extraction from the Actions runner process at publish time. The credential was synthesized inside the trusted pipeline, not phished. Second, the malicious @tanstack/* releases shipped with valid SLSA provenance. To my knowledge this is the first documented case of malware on npm carrying real, registry-honored provenance. The control that was supposed to make compromised pipelines visible became part of the camouflage.

The thesis below holds, with one extension: some of npm’s new controls are now part of the attack surface when initial access is upstream of the registry.

On March 19, 2026, a single threat actor compromised Trivy through a GitHub Actions misconfiguration. Five days later, the same actor pushed malicious versions of LiteLLM to PyPI. Three days after that, Telnyx. Then xinference on April 22. Then four official SAP packages on npm on April 29. Datadog Security Labs traces the chain to a group it calls TeamPCP; StepSecurity attributed the SAP compromise to the same group based on shared RSA public keys and encoding routines carried over from prior campaigns.

Six weeks. Two ecosystems. One playbook. That sequence is the cleanest evidence yet of what changed in 2025 and 2026: initial access decoupled from the registry. Attackers stopped optimizing for npm or PyPI and started optimizing for whichever publish token the victim happened to be holding.

The obvious read

The conventional story about 2025-2026 is volume. Sonatype’s January 2026 release puts new malicious packages at 454,600 for the year, a 75% year-over-year jump, with cumulative blocks now past 1.23 million. ReversingLabs pegs the same growth at 73% and attributes more than 99% of identified malicious open-source packages to npm. The npm Qix phishing wave alone touched packages totaling roughly 2.6 billion weekly downloads. Shai-Hulud 2.0 hit 796 packages and 132 million monthly downloads. Axios in March 2026 (covered separately on this site) added another high-profile name to a list that was already too long to read aloud.

That framing is correct but incomplete. The volume is real, and npm is taking most of it. But volume isn’t the structural story. The structural story is what the same threat actor does when handed credentials in two different ecosystems in the same week, and what each ecosystem’s architecture does or doesn’t do to contain it.

The pattern

Two threads converged in late 2025. First, attacker initial access moved upstream of the registry. The s1ngularity compromise in August 2025 didn’t exploit an npm flaw; it exploited a pull_request_target misconfiguration in an Nx GitHub Actions workflow that had been introduced five days earlier, stole the publishing token, and pushed eight backdoored versions. Ultralytics on PyPI in December 2024 followed the identical pattern from the other side: GitHub Actions script injection through pull requests, token exfiltration, four poisoned versions published in succession. The pypj.org phishing campaign in July 2025 and the Qix npmjs.help campaign in September 2025 are the same playbook running against different surfaces. The registry stopped being the target. The pipeline became the target.

Second, the payload itself became portable. The .pth mechanism on PyPI, a Python install-path file that runs on every interpreter startup, showed up as the persistence vehicle in both LiteLLM 1.82.8 and elementary-data v0.23.3, two of the TeamPCP-era compromises six weeks apart. On the npm side, Shai-Hulud 2.0 moved from postinstall to preinstall, and the Mini Shai-Hulud SAP compromise on April 29 wrote .claude/settings.json (abusing Claude Code’s SessionStart hook) and .vscode/tasks.json with runOn: folderOpen into developer repos it could reach, so the payload re-executes whenever an infected repo is opened. None of these techniques is registry-specific. They follow the credential.

What the data actually shows, when you line up the calendar, is that 2025-2026 wasn’t a year of independent campaigns. It was one operational model executed against two ecosystems with different defensive depth.

The evidence

The defensive divergence is real and asymmetric, and it’s worth being specific about where each ecosystem actually sits.

PyPI mandated 2FA on January 1, 2024. The December 2025 Year-in-Review reports 52% of active accounts on non-phishable factors (WebAuthn or passkeys), 50,000+ projects on Trusted Publishing accounting for roughly 20% of all uploads, and 17% of uploads carrying PEP 740 digital attestations. Project-level quarantine shipped in 2025 and demonstrated a 42-minute response window in the April 2026 PyTorch Lightning incident. Wheels bypass install-time code execution entirely. Pip resolves exact versions by default; Poetry and uv enforce hash-pinned lockfiles, and uv errors if the lockfile drifts from pyproject.toml.

npm revoked its classic long-lived tokens on December 9, 2025. That date is not incidental; the GitHub Changelog tied the revocation explicitly to Shai-Hulud. OIDC Trusted Publishing reached GA on July 31, 2025. Granular tokens cap at 90 days. Default Sigstore provenance attestations now ship with npm publish. These are real structural changes.

But npm still executes preinstall, install, and postinstall scripts unconditionally. Roughly 2.2% of published packages, about 33,000, define install lifecycle scripts, and typical transitive trees of 500-1,000 nodes mean any one of those runs on a normal install with no review. Caret ranges in package.json resolve to any 1.x.x on a fresh install unless package-lock.json is committed and honored in CI. pnpm v10 made blocking lifecycle scripts the default in January 2025, and pnpm v11 added a 24-hour minimumReleaseAge. npm and Yarn ship no equivalent default. The RFC for opt-in install scripts has been open for years.

The April 2026 single-maintainer audit of the 113 most-downloaded npm packages flagged 26 as CRITICAL for single-maintainer risk, collectively representing 10.3 billion weekly downloads. glob at 340M weekly downloads has had the same sole maintainer for 13 years. cross-spawn and esbuild sit at 190M each. The structural concentration of trust on npm has no PyPI parallel at that scale.

The Axios compromise (covered elsewhere on this site) is one more data point in the same pattern, not a separate problem. The two gaps it exposed, account-recovery-code bypass of 2FA and a v0.x branch publish workflow that was never migrated to OIDC, are exactly the gaps the structural argument predicts: npm shipped the new controls, but the old paths are still live and attackers know where they are.

What this means for prioritization

The asymmetry has a practical edge for anyone setting patching priority. If your dependency tree includes npm packages that are pulled fresh on every CI run, the realistic window between a maintainer’s account being compromised and your CI executing attacker code is the time it takes for one npm install to complete. Sygnia clocked 16 minutes from Qix credential capture to malicious publish. The Shai-Hulud worm’s mean propagation through a maintainer’s package set is measured in single-digit minutes per package. Detection windows for the Qix and s1ngularity compromises were two to five hours.

PyPI gives you more time and more chokepoints. The Ultralytics maintainers caught the initial compromise, published what they thought was a clean fix, then got hit again with the stolen token. That sequence is worse for the project but better for downstream consumers: the second wave didn’t auto-propagate through unlocked dependency trees the way the npm worms do, because pip doesn’t resolve floating versions by default.

The prioritization implication for a working team is concrete. npm-heavy stacks need install-time isolation as a default, not a security project. That means npm ci --ignore-scripts in CI, lockfiles committed with integrity fields honored, and a minimum release age (a week is the operational floor most teams can absorb) on anything that pulls fresh. PyPI-heavy stacks need to push past TOTP to phishable-factor 2FA on every maintainer account that still has it, verify attestations in release pipelines with pypi-attestations verify, and treat the 80% of uploads without Trusted Publishing as the unprotected surface they are. The 65% of new vulnerabilities lacking NVD severity scores at publication (Sonatype, 2026) means audit-based gating is increasingly blind to the actual threat. The compensating control isn’t a better scanner. It’s narrower default permissions at install time.

What to watch

Three things would either confirm or break this read in the next six months. First, whether npm ships any platform-level restriction on postinstall, opt-in or otherwise. The RFC has been open long enough that continued silence is itself a signal. Second, whether a PyPI-native self-propagating worm appears, originating from Python tokens rather than pivoting from npm. None has been documented; if one shows up, the “npm has the volume, PyPI has the structural backstop” framing weakens. Third, the September 11, 2026 EU CRA vulnerability-reporting deadline for manufacturers and OSS stewards. The conformity assessment isn’t due until December 11, 2027, but the reporting obligation lands first, and the organizations distributing OSS commercially are squarely in scope.

PatchDay Alert flags the maintainer-compromise advisories and the registry policy changes that move the floor, alongside the daily CVE feed, so the call about which dependency tree you’re carrying lands before the install does.

Sources

Share

Related field notes

One email, every weekday morning.

You're in. Check your inbox.

Get the digest

Free. Weekday mornings. Plain English CVE triage.

Check your inbox to confirm.