Derek: Welcome to Autonomous Autopsy. I'm Derek, he's Max, and today we are doing a full dissection of what might be the ugliest month in NPM history.
Max: And that is saying something.
Derek: So get this. On May 11th, the TeamPCP threat group hijacked TanStack's own CI/CD pipeline and pumped out 84 malicious package versions across 42 packages in six minutes flat, and every single one... Can we read valid SLSA Build Level Three provenance from Sigstore?
Max: Wait, wait, wait. Valid attestations on malicious packages?
Derek: Valid, attested, malicious.
Max: That's not supposed to be possible.
Derek: And yet we'll walk through exactly how the kill chain worked, how the worm self-propagated to over 170 packages hitting Mistral AI, UiPath, OpenAI got two employee devices compromised, and GitHub lost roughly 3,800 internal repositories. through a poison VS Code extension, Zero CVEs across the whole campaign.
Max: Zero, not one.
Derek: Oh, you're going to love this part. Running parallel to all of that was a separate campaign called TrapDoor. Socket disclosed it, Thirty-four malicious packages across NPM, PyPI, and Cratesio, and this one hid attack instructions inside .Cursor rules and CLAUDEmd files using Zero-width Unicode characters invisible to human reviewers but fully parsed by Cursor and Claude code.
Speaker 3: code. So the AI coding assistant becomes the delivery mechanism, the AI coding assistant becomes the delivery mechanism, and the attacker left behind an AUDIT-MATRIXmd file calling the whole thing a Universal AI Agent Extraction Framework.
Max: They documented it.
Speaker 3: They documented it. We'll dig into why traditional scanners had Zero coverage on any of this, what Socket caught and how fast, and then we close with Three things you can actually do Monday morning, not review your security posture. Specific steps. Let's get into it. Incident opening right now. May 11, 2026, 1920 UTC. Somebody opened the NPM registry and started pushing TanStack packages. Nothing weird there, right? Routine release. Except it wasn't routine at all. Forty-two packages, Eighty-four malicious versions, and the whole thing was done by 1926. Six minutes, Max. Six minutes. And zero alarms fired. Zero. Here's what's wild though. Nobody stole a password. Nobody phished a maintainer. The whole team had 2FA enabled. None of it mattered. Because the attacker never needed credentials. According to TanStack's postmortem, they used TanStack's own pipeline against it. This is where it gets good. Walk me through the chain. Okay, so three GitHub Actions weaknesses chained together. Step one, a misconfigured pull request target workflow. GitHub's security lab calls that pattern a "pwn request," forked code runs with base repo privileges, step two, the attacker's fork poisons the shared pnpm cache with a one point one gigabyte malicious store. Wait, back up. So they smuggled a payload into the cache during a low trust workflow run, then waited for the high trust release workflow to pick it up? Exactly.
Derek: And when a legitimate maintainer merged a totally unrelated piece
Speaker 3: Exactly. And when a legitimate maintainer merged a totally unrelated piece
Speaker 4: of code.
Speaker 3: In the related PR, the release workflow restored the poisoned cache. At that point, attacker code is running inside the privileged pipeline. And step three is honestly kind of elegant in a horrible way. The malicious code reads the OIDC token directly from the GitHub Actions Runner's process memory slash proc slash pid slash mem and uses it to publish straight to npm. No npm token.
Derek: and ever touched; so they minted a legitimate published token using the Runner's own memory. Right; and Snyk confirmed this is the first documented npm worm to produce valid SLSA Build Level Three provenance for malicious packages. Oh, that's fascinating in the worst way. Sigstore checked the certificates; they passed, because the packages genuinely were built by TanStack's own release.yml all on refs dot heads dot main." The attestation was technically accurate; the pipeline just had a passenger. Great feature! And here's the detail that really got me: according to Snyk's write up the malicious commit was authored under the name "Claude" impersonating the Anthropic Claude code GitHub app. Any devs scrolling Git history would have seen a familiar trusted name. That's deliberate social engineering baked into the commit!
Speaker 3: Commit metadata itself.
Max: Yep. CVE-2026-45321. CVSS 9.6. The package is passed every check defenders rely on. So TanStack was the entry point, but a worm with a valid publish token doesn't stop at one project. Where does it go from there?
Speaker 3: So with that OIDC token in hand, the worm didn't just sit on TanStack, it started eating the neighborhood. This is where the worm mechanic is genuinely different.
Derek: Mm-hmm.
Speaker 3: Most malware steals credentials and phones home. Mini Shai-Hulud use those stolen NPM and GitHub Actions tokens to publish malicious versions of every other package the compromised maintainer had write access to. So it's self replicating through the publish pipeline itself. Exactly. Every infected CI run becomes a new
Derek: The New Publisher. By end of day, according to SafeDeps analysis, the blast radius hit 170 plus packages across TanStack, mrleyi, UiPath, 65 packages just from UiPath, OpenSearch, and Guardrails AI.
Speaker 3: 518 million cumulative downloads across all those packages. That's the potential exposure surface. And here's the part I find most interesting. Triple C2. Wiz flagged it. The worm exfiltrated credentials three ways simultaneously: a typosquat domain called gittanstack.com, GitHub API dead drops using stolen tokens where it created Dune-themed repos as dead drops, and the Session decentralized messenger network. Wait, back up. Session is a P2P encrypted messaging app. There's no domain to block, no CDN to take down. How do you stop that? You largely don't at the network layer. Phoenix security noted that no CDN provider or certificate authority can take down Session's SNODE swarm. If your C2 detection is domain reputation based, you have zero visibility.
Derek: Oh, that's fascinating in a really uncomfortable way.
Speaker 3: Yeah, and then there's the blast radius question. OpenAI disclosed two employee devices hit via the TanStack compromise, credential-focused ex-
Derek: Exfiltration from a limited subset of internal repos, their words, they had to rotate code signing certificates for ChatGPT Desktop, Codex in Atlas, and then GitHub, according to BleepingComputer and confirmed by GitHub CISO Alexis Wales, a poisoned build of the Nx Console VS Code extension live on the official marketplace for eighteen minutes gave Team PCP enough access to pull approximately thirty eight hundred internal repositories. Eighteen minutes-eighteen minutes-and Team PCP put the source code up for sale, fifty thousand dollars on the breached forum. No CVEs across any of this, by the way. Phoenix Security flagged it: traditional CVE based scanners had zero detection surface on this entire campaign. Great, so your scanner gave you a clean bill of health while the worm published itself into a hundred and seventy packages. And that's the thing about Mini Shai-Hulud stealing credentials at the Credentials at this scale, TeamPCP was simultaneously running a quieter operation targeting the same credentials through a completely different door-that's TrapDoor-and it goes after the tools developers trust most. Parallel campaign: same week, TrapDoor started May nineteenth, and, according to Socket, it put thirty-four malicious packages across npm, PyPI, and Crates.io; three hundred and eighty-four artifact versions total. Numbers we haven't touched yet." And the packages were impersonating stuff developers actually reach for: crypto dev tools, AI utilities, env loaders, right. But here's the thing, Max: the credential theft is almost secondary to what they actually built into the package. into the payload. Oh, this is the part. Yeah, this is the part. The shared NPM payload TrapDoor.js plants .cursor rules and claude.md files in your project directory. And if you're not a Cursor or Claude code user, those file names mean nothing to you. They're just config files. Config files your AI assistant reads automatically every time it opens the project. So what's actually in them? Nothing. Visually. Zero-width Unicode characters. U200B U200C U200D and UFEFF. Standard text editor shows you a blank file. Wait, wait wait wait. The instructions are there, the editor just doesn't render them? The editor doesn't render them, but Cursor and Claude code parse the full Unicode stream-they see the instructions plain as day.
Max: Wild!
Derek: So a human reviewer looks at the file, sees nothing; the AI reads an entire attack playbook-exactly, and the playbook is elegant in the worst possible way. It tells the Assistant to run what looks like a security scan. A security scan. The attacker is asking your security tool to do the exfiltration. SSH keys, AWS credentials, GitHub tokens, crypto wallet key stores, the developer sees nothing, the Assistant does the work. Your AI is very thorough, very diligent, extremely helpful. And then there's the Rust angle. The crates.io package has abused build.rs, which runs automatically during cargo build. Build. You don't call it the build system does, and most tooling doesn't inspect ROS build scripts for outbound network calls. According to the cross analysis, the exfiltrated data was XOR-encrypted with a hardcoded key before it went out to GitHub Gists, so the exfil channel is GitHub. The C2 for many Shai-Hulud was a decentralized network, and this one uses gists, both invisible to anything blocking on domain reputation. Two different operations, same blind spot. Which, honestly-
Speaker 3: That's the thread that pulls segment four together. Zero CVEs, zero scanner hits, and the only thing that caught any of this was behavioral detection with a five-minute and 56-second average response time.
Derek: Zero CVEs across nine weeks of documented TeamPCP activity. Not one. Your scanner is up to date, your version is pinned, your SCA tool says all clear, and you're already compromised. That's the punchline of this whole thing. Traditional tooling had nothing to catch. No signature, no known bad hash, no CVE to alert on. These packages came from attacker-controlled accounts, so version pinning offered zero protection. You were pulling the legitimate looking artifact, and here's what makes this survivable at all—Socket. According to Socket's own data, they detected trapdoor releases with an average detection time of five minutes and fifty-six seconds across three hundred eighty-one package version records.
Speaker 3: Under six minutes. Behavioral and cross-registry analysis. Not signatures. They watched for packages exhibiting credential-harvesting behavior at install time across multiple registries simultaneously. Lastly, that's the only reason any of this surfaced. Wait, back up, because that distinction matters a lot. Signature-based tools are checking, have we seen this before? Socket was asking, what is this actually doing, exactly? And the cross-registry piece is key. The Crates.io wave showed infrastructure overlap with the npm and PyPI packages. One registry alone, you might miss it. Watching all three together, the behavioral pattern becomes obvious fast. Okay, so get this. The attackers apparently knew their operation well enough to document it. Socket found an AUDIT-MATRIX.md file sitting in the attacker's own GitHub Pages repository. They wrote a spec. They wrote a spec! According to Socket, this document describes the operation as a Universal AI Agent Extraction Framework, staged workflows for capability detection, data extraction, self-replication fallback, and telemetry reporting. So the attackers had better internal documentation than half. Half the security team's trying to stop them. That's genuinely funny, and also a little terrifying. The detection gap here points to something structural. AI coding tools read files (.cursor rules, .CLAUDEmd) that security tooling has never been asked to inspect. Your scanner doesn't know that file exists in a threat context. And that's not just a dev machine problem. That same trust model. Agent reads a file, agent acts on it. That's exactly how RAG pipelines work in production. The attacker doesn't need code execution—they need to get their content into something the agent reads.
Derek: Which means the attacker surface is basically every document in the context window.
Speaker 3: Yeah, and that's where this gets structurally worse than any CVE. So here's the thing: that Cursor rules mechanic isn't a weird one off, it's the same attack as poisoning a RAG pipeline, structurally.
Speaker 5: Say more.
Speaker 3: An attacker injects content into a source the AI reads and trusts. The AI acts on it as legitimate context. The developer or the agent never sees the instruction. That's TrapDoor. That's also a poison document in your knowledge base.
Derek: Right, and the OWASP Agentic AI Top 10 of 2026 actually has a dedicated category for this, ASI06, Memory and Context Poisoning. The key finding there is that agents cannot reliably distinguish instructions from data, which is the whole problem. Any document, email, or config file While in the agent's context window is a potential injection vector, you don't need to exploit a CVE, you just need to get your text in front of
Max: Wow.
Derek: the model. And here's where TrapDoor gets genuinely uncomfortable, Derek. That dot Cursor rules file-even if a developer nukes the malicious package with npm uninstall-the file is already written to the project root. Oh, the persistent survives the uninstall? Survives the uninstall. So the next time anyone opens that directory in Cursor
Speaker 4: -
Derek: VS Code, the AI coding assistant parses those hidden instructions fresh, new session, same payload. Oh, that's fascinating in the worst possible way-you clean up the infection vector in the footprint stays, now scale that to production: your CI runner installs a compromised package, the malicious code writes a poisoned file to your repo, and every AI tool that reads that repo is now operating under attacker controlled assumptions-you never pushed a bad commit. The agent just started acting differently.
Speaker 3: And your RAG index is pulling from that same repo.
Derek: Exactly. The agent reads the poison doc, treats it as ground truth, and now it's exfiltrating or misbehaving across sessions that have nothing to do with the original infection. The attacker doesn't need a foothold in your infrastructure; they need a foothold in your context window. That's the reframe. TrapDoor made it tangible on a developer laptop. But the threat model for anyone shipping agents with RAG pipelines is the same mechanic at production scale.
Speaker 3: Which means the controls you need aren't just about the registry; they're about what your agents are allowed to read and act on, and whether you'd even know if something had changed. Speaking of which, there are three specific things you can do right now, and one of them takes about 30 seconds. All right, three controls starting now. Max, give them the first one. Run this command on every repo you're actively developing in, cat dash v on your dot cursor rules to your Claude dot md, your agents dot md, all of them. Non-printing characters show up immediately. And if you see anything weird, rotate credentials first, investigate second. Don't get curious before you get safe. Good order of operations.
Derek: Second one, check for the mini Shai-Hulud persistent stamen. On macOS, that's tilde slash Library slash LaunchAgents
Speaker 3: Wow!
Derek: dot com dot user dot gh-token-monitor dot plist. On Linux, dot config slash systemd slash user dot gh-token-monitor dot service. And here's the thing people miss: remove it before you revoke the token, not after. If you revoke first, the daemon just captures the new one as you create. Once you create it, Youve solved nothing.
Speaker 3: Yeah, it's sitting there. Very patient malware. Okay, Third one. This is the one Id do today if I only had ten minutes. Lock your pull_request_target workflows. A workflow triggered by that event runs in your base branch context, which means it has access to your secrets and write permissions. So a fork PR just inherits privilege? Thats exactly what happened with TanStack. Untrusted fork code executed in a privileged runner. And the token was right there. Wait, back up. The fix here is actually simple, though, right? Genuinely simple. Switch the trigger to pull_request instead.
Derek: If you absolutely need pull request target, add a condition that checks the PRs from the same repo, not a fork. github.event.pull_request.head.repo.full_name equals github.repository. One that check. One line of YAML.
Speaker 3: Mm-hmm.
Derek: Breaks the attack chain at step one.
Speaker 3: one. Fewer than the four hundred and four malicious versions SafeDeps counted in that coordinated registry hit. Wildly fewer. Three controls: audit your AI config files, kill the persistent stamen before you rotate tokens, and lock your pull request target workflows. One of those three you can do in the next hour, no excuses. All right, that's a wrap on this one. Yeah; six minutes, Eighty-four malicious packages, zero alarms; that stat is going to stick with me. And the thing that honestly floored me, the SLSA provenance was valid;
Derek: the pipeline just had a passenger.
Speaker 3: That's the one; and the trap door on top of it, poisoning dot Cursor rules in Claude dot me with invisible Unicode your editor will never catch. The attack surface now includes the AI sitting in your In your dev loop (that's the takeaway from today), Monday morning actual item: run cat.v on every AI config file your team didn't write themselves. And pull request.target lock it down.
Derek: Untrusted fork code should never run in a privileged context.
Speaker 3: Both of those are free, no excuse. Thanks for being here, everyone. New episodes every Tuesday. Subscribe wherever you're listening. And if this saved you from a bad deploy, leave us a review. View. We will read it. See you next week.