All Articles
DevSecOps
Cloud Security
Startup Engineering
Cloud Engineering
IAM

Valid Provenance, Malicious Package: What Attestations Buy You

In May 2026 a worm shipped malicious npm and PyPI packages carrying valid SLSA Build Level 3 attestations. What provenance proves, what it never proved, and the controls that hold.

Avinash S
September 7, 2026
12 min read
Prefer us on Google
Illustration for Valid Provenance, Malicious Package: What Attestations Buy You, covering DevSecOps, Cloud Security, Startup Engineering

On 11 May 2026 a worm published 84 malicious versions across 42 TanStack npm packages. Inside 48 hours it had reached 172 packages and more than 400 malicious versions spanning npm and PyPI. Every one of those versions carried a valid SLSA Build Level 3 provenance attestation, signed through Sigstore, verifiable by any tool you point at it (Tenable, VentureBeat).

That is the part worth sitting with. The attestations were not forged. They were genuine, issued by the real build system, for the real repository, and they described exactly what happened: this artifact was built by this workflow from this commit. The commit was malicious.

This post is for the person running engineering at an early-stage startup with no dedicated security hire, who switched on provenance verification because a checklist said to, and who now needs to know what that control actually covers. It is not an argument against SLSA. It is an argument about where attestation sits in a defence stack, and what has to sit around it.

Most coverage framed the incident as signatures failing. Signatures did not fail. They worked, and they told the truth. What failed was the inference a reader makes from a green check mark.

Where provenance stood before this

Package provenance went mainstream quickly. npm shipped trusted publishing with OpenID Connect to general availability in July 2025, so a package can be published straight from a CI workflow with no long lived token in a secret store (GitHub changelog, npm docs). PyPI accepted PEP 740 and turned on digital attestations by default for projects publishing through trusted publishing with the PyPA action (PEP 740, PyPI blog).

The pitch was reasonable and largely correct: stop trusting a tarball because a name you recognise uploaded it, start trusting it because a public log records which workflow, in which repository, at which commit, produced it. That was a real improvement over an unverifiable upload. The May 2026 campaign did not undo it. It located the edge of it, in public, at scale.

1. What a Build Level 3 attestation actually claims

SLSA describes build integrity, in levels. Build Level 3 asks for a hardened, isolated build platform whose provenance cannot be falsified by the build itself. The specification is explicit that the level describes the integrity of the build process and the top level source, and says nothing about the artifact dependencies or the quality of the source code that went in (SLSA levels). The threat model page is equally direct about which threats sit outside its scope (SLSA threats).

Read that as a sentence a lawyer would accept. The attestation asserts provenance: this binary came out of that build, from that commit, on that platform. It does not assert intent, safety, or review. A build platform doing its job perfectly will faithfully attest a build of hostile source.

Takeaway: an attestation answers where an artifact came from. Nothing in the specification claims it answers whether the artifact should be trusted, and any control you built on that second reading was never supported by the document.

2. How the May 2026 worm earned a genuine attestation

The campaign, tracked as Mini Shai-Hulud and assigned CVE-2026-45321 with a CVSS score of 9.6, started in a continuous integration configuration rather than in a registry (Tenable). Public analysis describes an attacker forking the target repository and opening a pull request that triggered a workflow running on the pull_request_target event, which executes in the base repository trusted context with access to its secrets (The Hacker News, Expel). The maintainers published their own postmortem (TanStack).

From inside that trusted context the payload could reach the workflow identity token, exchange it for publish rights, and release. Because the release genuinely ran on the real build platform from the real repository, the provenance produced was genuine too. Analysis from the Cloud Security Alliance research team called it the first documented worm producing legitimately attested malicious packages (CSA Lab Space).

Takeaway: the attacker never attacked the signing system. They walked into the room that does the signing, which is a far cheaper attack and one your own pipeline may be equally open to.

3. The second wave, and why the pattern matters more than the incident

Eight days later, on 19 May 2026, a compromised maintainer account published 637 malicious versions across more than 300 packages in an automated burst reported at roughly 22 minutes, hitting packages with millions of monthly downloads (SafeDep). The payload again exchanged workflow identity tokens for publish tokens and signed through the public Sigstore infrastructure using the stolen identity.

Two campaigns, eight days apart, both producing cryptographically signed releases that verification tooling accepted. The recurrence is the finding. A one off would be a curiosity; two independent runs of the same play inside a fortnight means the technique is now standard, and that any control whose logic is attestation present, therefore proceed is a control that will keep passing hostile code.

Takeaway: treat identity theft in the pipeline, not signature forgery, as the live threat. The attacker goal is to become you for one release.

4. Why the verification step cannot fail for the reason you want

A verification is only useful if there is a realistic outcome in which it fails and tells you something. Provenance verification fails when an artifact was built somewhere other than the declared repository and workflow. That is a genuine class of attack and it is worth blocking. It is simply not the class of attack that ran in May 2026, in which the declared repository and workflow are exactly where the malicious build happened.

Practitioner opinion: this is the same mistake teams make with a green scanner dashboard. The check is real, its scope is narrow, and the comfort it produces is much wider than its scope. The fix is not to remove the check. It is to write down, next to the check, the sentence describing what a failure would have meant, and then find controls for everything outside that sentence.

Takeaway: keep verifying provenance, and stop counting it as coverage for malicious code. It covers substitution, not intent.

5. Control one: put a cooldown on dependency installs

Compromised versions tend to be found and pulled within hours. The install that hurts you is the automated one that happens during that window. Every major Node package manager now supports a minimum release age, so a version younger than your threshold is simply not selected. npm added min-release-age in the 11.10.x line, Yarn has npmMinimalAgeGate, and Bun has minimumReleaseAge (Socket). Renovate exposes the same idea for pull request automation (Renovate docs).

How does your infrastructure stack up?

Take the 2-min security quiz →

In npm this is one line in .npmrc, for example min-release-age=7, applied in continuous integration as well as on laptops or it does nothing where it matters most. Seven days is a reasonable starting point for application dependencies. Pair it with a documented override path, because you will occasionally need a same day security release, and a control with no escape hatch gets disabled in an incident.

Takeaway: a cooldown is the highest value control per minute of work on this list, because it defends without needing you to detect anything.

6. Control two: close the trigger that started this

GitHub documents the risk plainly. A workflow triggered by pull_request_target receives the base repository token and access to repository and organisation secrets, and gets write permission unless a permissions key narrows it, even when the pull request comes from a fork (GitHub docs). The GitHub Security Lab wrote up the exact failure mode years ago (Preventing pwn requests).

The rule that holds: a privileged trigger must never check out or execute fork controlled code. Split the work. Run untrusted code on the ordinary pull_request trigger with no secrets, and do the privileged part separately against data, not code. Set an explicit least privilege permissions block on every workflow, pin third party actions to a full commit SHA rather than a moving tag, and keep publishing in its own workflow with its own environment and approval (GitHub secure use reference).

Takeaway: audit your repositories for pull_request_target and workflow_run today. This is a grep, and it is the single highest yield hour in this post.

7. Control three: trusted publishing and staged publish

Trusted publishing removes the long lived npm token from your secret store and lets a specific workflow in a specific repository publish through a short lived identity exchange (npm docs). That is a real reduction: a leaked token from a laptop or a log no longer buys a release. It does not help when the attacker is executing inside the trusted workflow itself, which is what happened in May.

Staged publishing is the part that addresses this directly. A version is uploaded but not made live, so a human promotion step and registry side malware scanning sit between the build and the world (npm docs). If you publish packages, turn both on. On the Python side, publish through trusted publishing with the PyPA action so attestations are produced by default under PEP 740 (PyPI blog).

Takeaway: trusted publishing shrinks the credential you can lose, staged publishing adds the pause that lets a mistake be caught. Neither replaces control two.

8. Control four: make a stolen cloud credential worth less

The published payload analysis is a useful inventory of what a compromised runner gives up: cloud keys from environment variables, configuration files and instance metadata, Kubernetes service account tokens, HashiCorp Vault tokens, GitHub personal access tokens, npm tokens and SSH keys (SafeDep, Expel). Every one of those is a static secret sitting where a build step can read it.

Replace the static ones. Federate your pipeline into the cloud account with OpenID Connect so the workflow assumes a role and receives credentials that expire, instead of holding an access key pair (GitHub docs, AWS IAM docs). Scope the role to what the pipeline genuinely does, per repository and per environment. A short lived credential still leaks, and the difference is that the window in which it is useful closes on its own.

Takeaway: assume any secret readable by a build step is already gone, and buy back the loss with expiry and scope rather than with a promise that the runner is clean.

9. Control five: shrink what an install is allowed to do

Installs run code. npm lifecycle scripts execute on install by default, which is why a poisoned version can act the moment a build pulls it. The ignore-scripts configuration turns that off, and the sensible pattern is to disable scripts in continuous integration where nothing needs them, allowing them narrowly where a package genuinely requires a native build (npm config docs).

Alongside that, commit lockfiles and install from them with npm ci so a build resolves to exactly what you reviewed, keep publishing credentials out of any job that installs dependencies, and treat runners as disposable so nothing survives a job. Practitioner opinion: for a team of five to fifty engineers, the highest leverage version of this section is separating the job that installs the world from the job that holds the keys, because that separation degrades several attack paths at once.

Takeaway: the install step is code execution with your credentials in reach. Give it fewer credentials and less permission to run things.

10. If you think you were hit, the first hour

Order matters. Isolate before you rotate, because pulling tokens from a machine an attacker still controls tells them you are watching without removing their access. Public response guidance from the May campaign converges on the same sequence: contain affected machines and runners, then revoke and reissue GitHub tokens, npm tokens, cloud access keys, Vault tokens and Kubernetes secrets, then reprovision runners and containers rather than cleaning them (Tenable).

For detection, search lockfiles and manifests for the affected versions, and check for the persistence artefacts reported in the campaign, including gh-token-monitor.sh and a matching launch agent property list on macOS. Community scanners published for this incident encode those indicators (mini-shai-hulud-scanner). Assume every continuous integration run during the exposure window is suspect until you have evidence otherwise.

Takeaway: write this sequence down now, while nothing is on fire. The hour you spend on a one page runbook is the difference between a contained incident and a rotation you perform twice.

The controls, side by side

ControlWhat it stopsWhat it does not stopTypical effort
Provenance verificationArtifacts built outside the declared repository and workflowMalicious code built inside it, as in May 2026Hours
Dependency cooldownInstalling a compromised version inside its short live windowA compromise nobody reportsUnder an hour
Trigger hygiene on CIFork controlled code running with your secretsA compromised maintainer accountOne to two days
Trusted and staged publishingLeaked publish tokens, and unreviewed live releasesCode executing inside the trusted workflowHalf a day
Short lived cloud credentialsLong term value of any credential a runner leaksDamage inside the credential lifetimeOne to two days
Install hardeningLifecycle script execution and drifted resolutionMalicious code in a package you then runHalf a day

Where to start, by stage

Pre-seed, one to ten engineers. Do two things this week. Set a dependency cooldown in .npmrc and in your CI configuration, and grep every repository for pull_request_target and workflow_run, removing any that check out fork code. Both are short, neither needs a new tool, and together they close the exact path used in both May campaigns.

Seed, ten to thirty engineers. Add the credential work. Move cloud access in CI to OpenID Connect federation with scoped roles, put explicit least privilege permissions blocks on every workflow, pin third party actions to commit SHAs, and separate the install job from any job holding publishing credentials. If you ship packages, switch to trusted publishing with staged publish.

Series A and beyond. Make it institutional. Track which repositories still carry static secrets, enforce the trigger and pinning rules through an organisation policy rather than review, keep an inventory of what your build identities can reach, and rehearse the rotation runbook once so the first hour is muscle memory rather than research.

Closing

Provenance is a control worth keeping. It answers a specific question honestly, and the answer it gave in May 2026 was accurate: these packages really were built by that pipeline. The work now is to stop asking it a question it never claimed to answer, and to put a cooldown, a locked down trigger surface, and expiring credentials around it so the next genuinely attested malicious release lands somewhere that can absorb it.

If you want a structured pass over your own pipeline, the MatrixGard cloud and security checklist walks the same ground in the order above. Open the checklist and start with the trigger audit, since it is the shortest item and the one that closed this specific door.

Avinash S is the founder of MatrixGard, a fractional DevSecOps practice for early-stage startups, funded or bootstrapped. He works on cloud infrastructure, pipeline security and cost control for teams without a dedicated platform or security hire.

Methodology. Every factual claim in this post is drawn from public sources published between 2024 and September 2026: vendor and registry documentation (npm, PyPI, GitHub, AWS), the SLSA specification, PEP 740, the maintainer postmortem, and incident analysis from Tenable, the Cloud Security Alliance research team, Expel, SafeDep, Socket and reporting outlets, all linked inline. Package counts for the 19 May 2026 burst are reported slightly differently across sources, between 314 and 317 packages, so this post states the figure as more than 300 and links the source. No client data, engagement outcome or private telemetry was used, and anything not traceable to a linked source is labelled as practitioner opinion.

MatrixGard

Ready to close the gaps?

MatrixGard finds what your team missed. Not because they're bad, because they're too close to the problem.

Book a free review

The notes, not the newsletter

One practical thing a week about running cloud infrastructure without a security team. Written by me, not generated.

Not an email person? One click, no signup: Google shows you our posts first.

Add MatrixGard as a preferred source