Lorem, ipsum dolor sit amet consectetur adipisicing elit. Qui, itaque voluptate ipsa non enim amet ducimus voluptatibus deserunt nam esse!
Inside npm's New Staged Releases: Defending Against Scripted Supply Chain Attacks

Inside npm's New Staged Releases: Defending Against Scripted Supply Chain Attacks

pr0h0
npmsupply-chain-securitygithubpackage-publishing
AI Usage (84%)

When I release a package, I treat publish time like a production deploy. That is the point where maintainer identity, build output, and registry metadata line up in a way downstream users trust immediately.

According to the report, GitHub has added staged publishing to npm to make that moment less risky. The basic idea is simple: if a malicious publish is not public the instant it lands, there is time for review, detection, and reversal before the package starts spreading through installs and update checks.

That is not a silver bullet. It does not make npm safe by default. What it does is slow the kind of scripted supply chain attack that depends on speed, automation, and the assumption that nobody will notice in time.

What GitHub changed with staged publishing in npm

The release gap between upload and public availability

The useful part of staged publishing is the gap itself.

Instead of turning a publish into an immediate public event, the release enters a staged state first. That gives maintainers and security reviewers time to inspect the package before it is promoted to the version users actually see. In practice, that means checking the artifact against the repository state, reviewing release metadata, and flagging anything odd before the package reaches the broader ecosystem.

That delay matters because npm release abuse usually depends on instant propagation. Once a malicious version is public, package managers, CI systems, and developers running npm install can start pulling it in almost immediately. A staged step interrupts that chain. The package has to survive one more human checkpoint before it becomes part of the dependency graph.

I like this change for one reason: it turns publishing from a single irreversible gesture into a two-step process. That is much easier to defend.

Why this matters for package maintainers and downstream users

Package maintainers are not just shipping code. They are guarding a trust boundary.

A release can carry application code, build output, install scripts, and metadata that downstream systems consume automatically. If a maintainer account is compromised, or a release token gets stolen, the attacker is not limited to the repository. They can push code into the ecosystem that other apps will ingest without human approval.

Downstream users feel that risk fast. Many installs use semver ranges, not pinned hashes. Many CI pipelines trust the registry more than the repo. Many developers assume that a new package version is safer because it came from a known maintainer. That assumption is exactly what scripted supply chain attacks exploit.

Staged publishing helps in two places:

  • it gives maintainers a chance to catch the problem before the release is public
  • it reduces the speed advantage an attacker gets from automation

It does not remove trust. It makes trust harder to abuse at scale.

Why scripted supply chain attacks love the publish step

Stolen maintainer credentials and abused CI tokens

The publish step is attractive because it sits at the intersection of identity and distribution.

An attacker who gets a maintainer password, a session token, or a CI secret does not need a complicated exploit chain. They need a release workflow. In many projects, that workflow is already privileged enough to build, sign, tag, and publish the package. A stolen release identity is often more valuable than a direct code execution bug.

The usual entry points are boring, which is why they work:

  • phishing a maintainer into entering credentials on a fake login page
  • stealing an npm token from a leaked .npmrc
  • abusing a CI job that has write access to the registry
  • compromising a machine that holds browser sessions, SSH keys, or environment secrets

Once the attacker has that access, the publish step is low-friction. The package can be rebuilt, repacked, and pushed with very little manual effort. If the maintainer is used to publishing from a laptop or a GitHub Actions job, the attacker can imitate that same path.

How attackers use fast, automated publishes to beat human review

Speed is the real weapon.

A scripted attack can modify the package, publish a new version, change the dist-tag, and move on before anyone opens the changelog. That matters even more if the package is popular, because dependency update bots and automated CI systems can pick up the release quickly. The attacker is not trying to stay hidden forever. They are trying to maximize the window where the bad version is circulating and the maintainer has not yet reacted.

That is why automation makes the attack more dangerous. The attacker can publish at scale, across multiple packages, with consistent naming and version patterns. They can try several timestamps, several channels, and several tags. Human review loses when the release process is a single click or a single command.

The defense is not to make publishing so hard that legitimate maintainers hate using it. The defense is to add one more checkpoint where the release can be compared against reality before it becomes public.

The old failure mode in npm release workflows

Immediate publishing from a compromised laptop or CI job

The classic failure mode is simple: a privileged environment gets compromised, and the next npm publish becomes the attacker’s delivery mechanism.

A lot of release pipelines still assume the machine that runs publish is trustworthy. That is a fragile assumption. If the laptop is infected, if the CI runner is poisoned, or if a secret leaks into logs, the attacker does not need to “break into npm.” They just wait for the next publish step.

This is especially risky when the package build and the package release happen on the same machine without meaningful review. A local build might pull in hidden files, stale artifacts, or a tampered dependency tree. A compromised CI job might package whatever is in the workspace rather than what is in the reviewed commit. If the release command is the final gate, then the last gate is also the weakest one.

I usually want release to be a clean, repeatable operation. If it depends on the exact state of someone’s laptop, I assume the process is too trusting.

Dist-tag manipulation, version races, and blind trust in new releases

A publish is not only about uploading bytes. It is also about moving trust around the registry.

The version number tells consumers where the package sits in history. The dist-tag tells them which version is current or stable. If the release process is sloppy, an attacker can abuse either one. A malicious release might not need a dramatic version jump. It only needs to become the version that install automation sees first.

This is where blind trust gets expensive:

  • many projects install with semver ranges like ^1.2.0
  • many users trust the latest tag without checking the release notes
  • many automated systems update packages before a human reads the diff
  • many maintainers assume that “new version from the official package” is enough proof

That is not enough. A registry tag can move faster than your review process. A version can be published before your alerting catches up. And if you do not verify the target channel, you can accidentally ship a release to the wrong audience.

How staged publishing changes the attack surface

Human-in-the-loop verification windows and anomaly detection

Staged publishing works best when the window is not passive.

If a release waits in a staging state, that creates room for human review and anomaly detection. The maintainer can compare the staged artifact to the repository commit. A release manager can inspect whether the tarball contains only expected files. A security reviewer can check whether the version bump matches the commit history and whether the release happened at a normal time.

That review can catch things like:

  • files added to the tarball that are not in source control
  • install scripts or lifecycle hooks that were not expected
  • a release generated from an unrecognized branch or commit
  • a publish that arrived from an unfamiliar CI actor or machine
  • a version bump that does not match the planned release

The value is not perfection. The value is forcing the attacker to survive more than one check.

Slowing down mass compromise and follow-on dependency abuse

The biggest practical benefit is the reduction in blast speed.

When a malicious publish becomes public immediately, it can be consumed by package managers, build systems, and dependency bots before the maintainer even notices. If the release is staged first, the attacker loses that instant propagation window. That gives defenders time to:

  • revoke tokens
  • disable compromised accounts
  • remove malicious build artifacts
  • notify downstream maintainers
  • inspect internal apps that depend on the package

That matters most for packages with a wide transitive footprint. A single compromised library can reach many internal and external applications through dependency trees. Slowing the release by even a short window can keep a small compromise from turning into a broad one.

What staged publishing does not stop by itself

Staged publishing is useful, but it is not a complete defense.

It does not stop a malicious maintainer who can approve the staged release themselves. It does not stop a compromised release manager who is already part of the approval flow. It does not stop an attacker who has inserted malicious code earlier in the source tree or build system. And it does not stop a stolen signing or publishing secret from being reused in a way that still looks legitimate.

It also does not help if the review is ceremonial. If nobody checks the staged artifact, the delay is just a pause before the same bad release becomes public.

That is why staged publishing should be paired with stronger identity controls and artifact verification, not used as a substitute for them.

Walkthrough of a safer maintainer release pipeline

Prepare release notes, version bumps, and artifact checks before publish

I prefer to treat the release as a documented sequence, not a reflex.

Before publishing, prepare the version bump, the changelog, and the build artifacts in a clean workspace. Make sure the intended commit is obvious. A good release pipeline should tell you:

  • which commit is being shipped
  • what changed since the last release
  • what files will be included in the package
  • which registry and tag will receive the release

A practical release flow usually starts with a locked dependency install and a clean working tree.

git status --porcelain
npm ci
npm run build
npm test

If the project uses generated artifacts, build them in the same environment you will use for publish. The goal is to make the package content predictable. If the output changes depending on who ran the build, you have a release integrity problem before you ever get to npm.

Inspect package contents with local dry runs and tarball comparison

This is the step I would not skip.

Create the package locally, inspect the tarball, and compare it to the source tree. That catches a lot of release mistakes before they become public.

npm pack --dry-run
npm pack
tar -tf *.tgz

A dry run tells you what npm thinks it will include. The actual tarball tells you what is really inside. If those two views differ from your expectations, stop there.

I also like to compare the tarball contents against the repository:

git ls-files
tar -tf package-1.2.3.tgz | sort

That comparison helps spot a few common problems:

  • accidental inclusion of secrets, local config, or test fixtures
  • missing build output because files is misconfigured
  • package metadata that points to the wrong entry point
  • lifecycle scripts that were not reviewed as part of the release

If you want a safer mental model, think of this as checking the exact bytes downstream users will trust, not just the code you happened to write.

Promote only after verifying provenance, dist-tags, and expected git state

The last step should be promotion, not discovery.

By the time you are ready to promote, verify that the git state matches the intended release commit and that the registry destination is correct. If your workflow uses a staging state before public release, do not approve it until the artifact, version, and metadata all line up.

The checks I would want before promotion are:

  • the commit is the one that was reviewed
  • the tree is clean and reproducible
  • the tarball contents match the planned release
  • the version number is correct
  • the dist-tag is the intended channel
  • the registry is the right one

If any of those are fuzzy, do not promote. It is much easier to fix a staged package than a public one.

Concrete checks to add before every publish

Confirm the package tarball matches the repository and build output

This is the simplest useful checklist, and it catches a surprising number of failures.

CheckWhy it mattersSafe command
Clean working treeAvoids shipping local junk or unreviewed editsgit status --porcelain
Locked installReduces build driftnpm ci
Dry-run packageShows included files before publishnpm pack --dry-run
Tarball inspectionConfirms what is actually shippedtar -tf *.tgz
File list comparisonDetects unexpected additions or omissionsgit ls-files vs tarball listing

A good habit is to fail the release if the tarball includes anything you did not expect. That includes secrets, temporary files, local docs, or build outputs from an old branch.

If you rely on the files field in package.json, test it every release. It is easy to misconfigure and easy to ignore until something embarrassing ships.

Verify the target version, tag, and registry destination

Wrong-version releases are common, and wrong-registry releases are worse.

Before publish, verify:

  • the version in package.json
  • the intended npm dist-tag
  • the registry URL
  • the identity or token being used for publish

A simple preflight check can help:

npm pkg get version
npm config get registry
npm view your-package-name dist-tags

If your workflow uses a private registry mirror or a separate publish host, be explicit. Do not rely on inherited config from a developer shell. I have seen release jobs inherit the wrong .npmrc more than once, and that is the kind of mistake that turns a clean release into a confusing incident.

Rehearse rollback, deprecation, and unpublish constraints

Do not assume unpublish is your emergency brake.

In real incidents, the first problem is often not “how do we remove it?” but “who already consumed it?” By the time a bad package is public, some downstream systems may already have cached it, installed it, or built against it. That is why rollback planning needs to happen before the release, not after the alert.

Your runbook should cover:

  • how to deprecate a bad version with a clear message
  • how to publish a fixed follow-up version
  • who is authorized to make that call
  • how to notify downstream consumers
  • how to record the incident for later review

A safe release process assumes the bad version may be permanent enough to matter. Your job is to reduce the chance it gets out, and to know what you will do if it does.

Hardening the surrounding workflow beyond staged publishing

Use trusted publishing and short-lived credentials where possible

Staged publishing helps, but short-lived identity is better.

If your release flow supports trusted publishing, prefer it over long-lived tokens sitting in CI secrets. Short-lived credentials shrink the window in which a stolen secret can be reused. They also make it easier to see which system actually performed the release.

The main security goal here is to remove static credentials from as many places as possible. A release token that lives forever in a repo secret is much easier to steal than an identity issued for one job, one run, or one environment.

For a release pipeline, that means:

  • avoid persistent npm tokens when a federated identity works
  • scope release-only credentials to the publish job
  • keep non-release jobs out of the credential path
  • log enough to audit identity use without leaking secrets

Enforce 2FA, branch protection, and least-privilege access

Identity controls still matter.

Two-factor authentication should be mandatory for anyone who can publish a package or approve a staged release. Branch protection should keep release branches from being pushed casually. Code owners and review rules should make sure a release commit is not merged by accident. And automation should have only the permissions it needs, not full maintainer rights by default.

This is one place where least privilege is not a theory. It is the difference between a token that can run tests and a token that can ship the next compromise.

Rotate secrets and treat release automation as a high-risk system

I would also treat release automation as a high-risk system, not a convenience script.

That means:

  • rotate npm and CI secrets regularly
  • audit .npmrc files and environment injection
  • isolate release jobs from general build jobs
  • keep logs clear of tokens and registry credentials
  • review changes to release automation with the same care as application code

A release workflow is production infrastructure. It deserves the same discipline as any other system that can affect customers immediately.

What to monitor for suspicious package activity

Alert on unusual publish timing, version jumps, and new maintainer access

Monitoring helps when prevention fails.

Useful alerts include:

  • publishes at unusual hours for the project’s normal pattern
  • version jumps that do not match the roadmap
  • new maintainers or collaborators added unexpectedly
  • changes to package metadata, especially maintainers and dist-tags
  • repeated publish attempts from unfamiliar accounts or locations

The goal is not just detection after damage. It is faster confirmation that something is wrong while the release is still in the staged or early-public phase.

Watch CI actors, token use, and unexpected dist-tag changes

CI is often the easiest place to hide malicious release activity.

Watch for:

  • publish jobs running under a new actor
  • token usage from a job that should not publish
  • changes to GitHub Actions or other release workflows
  • dist-tag updates that happen without a corresponding release note
  • registry changes that do not match the expected branch or commit

If you are a maintainer, it is worth regularly checking the active dist-tags for your packages and comparing them to the release plan. A malicious tag move can be just as damaging as a bad new version.

Track blast radius across internal apps and transitive dependencies

You cannot defend what you do not know depends on you.

For maintainers, that means understanding which internal projects consume your package and which downstream teams are most exposed to a bad release. For application teams, it means inventorying package dependencies well enough to know what would break if a key library became malicious.

Good dependency tracking gives you answers to questions like:

  • which apps use this package directly
  • which apps consume it transitively
  • which lockfiles would need intervention
  • which environments auto-update dependencies
  • which teams should be notified first during an incident

That inventory is boring until you need it. Then it is the thing that keeps the incident manageable.

Where staged publishing fits in defense-in-depth

Compare staged publishing with signing, provenance, and policy gates

Staged publishing is one control, not the whole system.

ControlWhat it doesWhat it does not do
Staged publishingAdds a review window before public releaseDoes not verify code integrity by itself
ProvenanceTies the build to a source and build identityDoes not stop malicious source changes
SigningHelps verify package authenticityDoes not fix a compromised signer
Policy gatesEnforce approval before promotionDo not help if the approval process is weak

If you are building a serious release process, combine these controls. Staged publishing buys time. Provenance tells you where the artifact came from. Policy gates decide whether it should be promoted. None of them alone is enough.

Which teams still need manual release approval

Some teams can automate a lot of the path to publish. Others should not.

Manual approval still makes sense for:

  • packages with a large download base
  • libraries used by critical internal systems
  • packages maintained by a small or dispersed team
  • releases that change install-time behavior or network access
  • packages that ship to regulated or high-risk environments

If the package matters enough that a malicious release would be painful, then a two-person rule or explicit human approval is not overhead. It is part of the control plane.

Limits, failure modes, and likely attacker adaptation

Compromised maintainers and social engineering still matter

A staged release only helps if the reviewer is not the compromised identity.

If the attacker gets the maintainer to approve the staged release themselves, the window becomes cosmetic. If they phish the person who controls the publish gate, the delay does not save you. If they compromise the laptop that signs off on promotion, the approval is only as good as the device behind it.

That is why critical packages need out-of-band verification for release changes. For example, a sensitive version bump should be confirmed through a separate channel, or by a second maintainer who checks the diff and the tarball independently.

Attackers may move earlier into source, build, or secret handling

If publish becomes harder, attackers will move left.

Expect more focus on:

  • source repository compromise
  • build pipeline tampering
  • malicious dependency updates in the release chain
  • secret harvesting from CI logs, caches, and environment variables
  • social engineering around maintainer roles and access recovery

This is another reason not to oversell staged publishing. It makes one stage safer. It does not make the whole release chain trustworthy. If anything, it pushes the attacker to be more creative earlier in the process.

Maintainer checklist for adopting the new workflow

Before release

  • confirm the release commit is approved
  • run a clean install and build
  • inspect npm pack --dry-run
  • compare tarball contents to expected files
  • verify version, registry, and dist-tag
  • ensure 2FA and trusted publishing are enabled where possible
  • make sure rollback and deprecation steps are documented

During the staged window

  • review the staged artifact against the repository
  • check for unexpected files, scripts, or metadata
  • verify the publish actor and timestamp
  • confirm no last-minute secret or workflow changes landed
  • require a second reviewer for high-impact packages

After promotion and post-release monitoring

  • watch registry tags and download spikes
  • monitor for new maintainer access or workflow edits
  • alert on unusual CI activity tied to the release
  • track downstream breakage and dependency updates
  • deprecate or replace the version quickly if anything looks wrong

Conclusion

The interesting part of staged publishing is not the label. It is the pressure it puts on the release process.

A bad npm release used to be a single fast move: compromise a maintainer, run publish, and let the ecosystem do the rest. A staged release interrupts that rhythm. It gives maintainers a chance to look at the artifact before it becomes public, and it gives defenders a better shot at catching scripted attacks before they spread.

That still leaves plenty of work to do. You still need trusted publishing, 2FA, least-privilege access, tarball inspection, provenance checks, and a real rollback plan. But staged publishing is a good sign because it treats release as a security boundary instead of a build artifact shuffle.

That is the mindset shift I would want more package ecosystems to adopt.

Further Reading

Share this post

More posts

Comments