Secure the Source, Not Just the Package

php2shell patches (CVE-2026-109618 and CVE-2026-109619) for EOL PHP are out now. With sales replies slower than usual due to high demand, the fastest route to patched packages is to buy ELS for Runtimes online (opens in new tab). Read the patch FAQ

Go straight to an AI summary of this blog post instead

  • Summarize this post with Perplexity
  • Summarize this post with Gemini

Key Takeaways

  • npm provenance proves which repository, commit and workflow produced a package, not that the code is safe to run.
  • The attestation gap is the space between what provenance proves and what you actually need to know before you install something.
  • A hijacked workflow or a malicious commit still produces valid provenance, and most npm packages carry no provenance at all.
  • Rebuilding packages from verified source in a hardened environment moves the root of trust away from the publisher’s pipeline.

If you’ve poked around npmjs.com lately, you’ve probably noticed it. Near the bottom of a lot of package pages, there’s now a provenance section saying the package was built and signed on GitHub Actions, with a link straight to the commit it came from. It looks like a seal. It feels like the problem is handled.

Plenty of teams treat that seal as the finish line, and honestly, I get it. Provenance was a real step forward for the ecosystem. But it answers a much narrower question than most people assume, and the space between the question it answers and the one you actually care about is exactly where a lot of recent npm attacks have been living.

What npm provenance actually proves

npm introduced provenance in April 2023, and the arrival of trusted publishing in July 2025 made it close to automatic. When a maintainer publishes from GitHub Actions through trusted publishing, the npm CLI generates a provenance attestation by default, no flag needed. That attestation is a signed SLSA statement, recorded in Sigstore’s public transparency log, tying the tarball to a source repository, a specific commit and the workflow that built it. If you’re on the consuming side, the npm audit signatures command checks those attestations across your installed tree.

Those are useful facts to have. Before provenance, the repository link on a package page was whatever the maintainer typed into package.json. Nothing verified it and nothing pinned it to a commit. And because npm publish packs whatever happens to be sitting in the publisher’s working directory, a tarball could contain build output, stray files or something far worse that never existed in git at all. For packages that use it, provenance closes that hole. You know the tarball came out of a particular CI run against a particular commit.

What you don’t know is whether anything inside that run deserved your trust.

The attestation gap

It helps to give this thing a name, so I’ll call it the attestation gap. The attestation gap is the difference between what a provenance attestation proves (which repository, commit and build workflow produced a package) and what you actually need to know before installing it, which is whether that artifact is safe and faithfully built from source you’d be comfortable running.

Provenance is a claim about origin. Safety is a claim about content. An attestation can be cryptographically perfect and still say nothing about what the code does when it lands in your node_modules folder. If an attacker gets their code into the commit, or into the build, they get a valid attestation for free.

How a package passes provenance and still burns you

The simplest route is a compromised maintainer. If someone takes over a GitHub account with push access to a project, they don’t need to touch npm at all. They push a commit, the release workflow runs exactly as it was designed to, and the new version ships with provenance pointing faithfully at the attacker’s commit. Every check passes, because nothing about the publish itself was unusual. The problem lives in the commit, and provenance was never built to judge commits.

The second route goes through the build pipeline itself. Trusted publishing swaps long-lived tokens for short-lived ones minted by the CI workflow, which is a big improvement. It also means the workflow’s identity becomes the credential. Andrew Nesbitt’s review of GitHub Actions compromises in Python packages found cases where the attacker ended up holding a valid token minted by the project’s real workflow, the one situation trusted publishing can’t do anything about. The ecosystem is different, but the mechanics carry straight over to npm.

The third route is the one people forget about. Your release build pulls in its own dependencies, things like bundlers, transpilers and plugins, all resolved fresh on the CI runner. If one of those is compromised when the release job runs, its payload can end up in your dist folder and ship inside a package whose provenance is spotless. The attestation names your commit. It doesn’t vouch for the hundreds of packages your build step installed on the way to producing that tarball.

Missing provenance is a signal nobody is watching

This next part bothers me more. Provenance is opt-in, and most of the registry hasn’t opted in. It isn’t even available for packages built from private source repositories. And npm install will happily install a version with no attestation, even when every earlier version of that package had one.

Now think about how the big worm campaigns work. Shai-Hulud and the waves that followed it went after npm tokens and GitHub credentials so they could republish legitimate packages automatically. Someone holding a stolen token can publish from anywhere, which usually means no CI run and no attestation. In theory, that’s a giveaway. When PyPI wrote up its analysis of the ultralytics compromise, they noted that the second wave of malicious releases stood out because it had no matching repository activity and no publish attestations.

That works if someone is looking for the drop. In most npm setups, nobody is. A package that suddenly loses provenance looks exactly like the thousands that never had it, and your install goes through without a murmur.

Rebuilding from source closes a different gap

If provenance tells you where something was built, the obvious next question is whether you can build it yourself and get the same thing. That’s the idea behind an independent rebuild. Rather than trusting the tarball the publisher uploaded, you take the source it claims to come from, build it again in an environment you control, and treat your output as the thing that gets installed. The publisher’s pipeline stops being the root of trust.

That’s the approach we took with SecureChain. Packages are rebuilt from verified source in hardened, isolated environments, with no inherited binaries and no opaque upstream artifacts. Each one is also screened for known malware and suspicious code patterns, and anything that fails inspection is blocked before it reaches a developer laptop or a CI runner. It works as a drop-in replacement for the public npm registry, so npm, Yarn and pnpm all behave the way your team expects, and it can sit upstream of Artifactory, Nexus or GitHub Packages if that’s how you already proxy packages.

SecureChain ships SLSA Level 3 provenance too, which might look like I’m undercutting my own argument. I’m not. Provenance is only as meaningful as the build it describes. Upstream provenance tells you the maintainer’s CI produced a package, and a compromised CI produces packages just fine. Provenance attached to an independent rebuild tells you the artifact came out of a hardened build environment, from source that was checked before it went in. It’s the same SLSA format in both cases, but it’s vouching for a completely different build.

What a rebuild can’t do for you

Rebuilding has limits, and they’re worth being upfront about. If the malicious code lives in the source itself, a faithful rebuild will faithfully reproduce it. That’s why rebuilding and malware screening have to travel together. One asks whether the artifact matches its source, and the other asks whether the source is something you want running on your machine. Neither question closes the gap on its own.

There’s an ecosystem-wide wrinkle too. Not every npm package builds cleanly from what’s in its repository. Some ship files that were never committed, some depend on build setups nobody ever documented, and a few have no public source at all. That’s a problem for anyone trying to verify the registry at scale, and it’s a fair argument for asking more of publishers, not less.

Where I’d start

The cheapest place to start is running npm audit signatures in CI so you can at least see which of your dependencies are attested and which aren’t. If you publish packages yourself, switch to trusted publishing so your own releases carry provenance. It costs you very little and makes the ecosystem a bit easier to reason about.

Then have the bigger conversation with your team about where your trust actually sits. Right now, for most JavaScript projects, it sits with thousands of strangers’ CI pipelines and npm accounts, and provenance just tells you which ones. Moving that trust to a source you control, one that rebuilds, screens and keeps patching packages long after their maintainers move on, is a much stronger position. That last part is where TuxCare’s Endless Lifecycle Support picks up once a package reaches end of life.

Provenance is worth having, and I’d like to see far more of the registry using it. It just answers where a package came from, and the question that keeps your CI secrets safe is what’s inside it.

Summary
Secure the Source, Not Just the Package
Article Name
Secure the Source, Not Just the Package
Description
npm provenance proves where a package was built, not that it's safe to run. Why that gap matters and how verified rebuilds close it.
Author
Publisher Name
TuxCare
Publisher Logo

Join 4,500+ Linux & Open Source Professionals!

2x a month. No spam.

Table of Contents
Get the open-source security answers you need

Join 4,500+ Linux & Open Source Professionals!

2x a month. No spam.