I promise you this is not yet another npm-worm-related article. There are plenty of them by now; even my Gen-Z niece has already heard about that in Roblox. If you did not, well, you could browse TikTok for new-age influencers explaining it poorly, or inform yourself by taking a look at some of these curated links below:
- Shai-Hulud: Self-Replicating Worm Compromises 500+ NPM Packages - StepSecurity
- Bugs in Shai-Hulud: Debugging the Desert – Aikido
- Updated and Ongoing Supply Chain Attack Targets CrowdStrike npm Packages – SocketDev
ℹ️
TL;DR: A postinstall payload harvests secrets with TruffleHog, drops a malicious GitHub Actions workflow, then republishes packages using any discovered npm tokens.
1.1. A slap in the face
This attack will be remembered forever in history as the first supply-chain attack capable of replicating, effectively making it a worm. This evidence shows how fragile the open source software ecosystem is and is forcing everyone to review their security practices. Don’t be confused, we’ll be speaking about the npm ecosystem, but it is not the only one that is under the exact same conditions.
This became such a complicated situation that it has prompted organizations such as the ones developing pnpm and yarn to add new settings allowing for delayed dependency updates (bun was considering it as well), and in the case of GitHub to plan a more secure npm supply chain.
1.2. What to expect from this article
The objective of this article is to —hopefully— leave you with enough knowledge to reduce your attack surface, harden your current development operations, and teach you to sand-walk in a way that you could potentially resist and avoid another ‘Shai-Hulud’ from striking you. Whether you’re simply developing your own application or publishing a package, we’ve got you covered.
Most articles cover the worm's behavior and affected packages, which is valuable work. However, many developers struggle to find practical guidance on prevention.
Friends across different fields noted the difficulty in finding actionable prevention steps. Most write-ups focus heavily on forensic analysis and propagation techniques rather than practical mitigation strategies.
After reviewing numerous sources, I found that typically 95% of content describes the attack, with only a few bullet points offering recommendations. These suggestions are often worm-specific or product promotions rather than general security improvements. Common recommendations include:
- "Contact your incident response team" — impractical for solo developers and individuals
- "Hire our services/products" — transparent marketing attempts
- "Reinforce phishing awareness" — irrelevant since the worm's propagation mechanism doesn't involve phishing
- Reactive rather than preventive measures — such as YARA rules and detection scripts
- "Rotate credentials" — an obvious but insufficient solution
- "Pin npm package dependency versions to known safe releases produced prior to..." — incomplete guidance (we have a dedicated section explaining the limitations of this approach)
- "Harden GitHub security" — stating the obvious without providing specifics
- Implement minimumReleaseAge — with misleading or incomplete official documentation
In reality, creating a secure development and publishing setup is a complex task. There’s no single advice that will solve your problem, but you’d need to integrate multiple measurements to achieve it. There’s an interesting thread in Hacker News from YC where you can see the confusion on how the npm system actually works.
To enhance your security posture today, let's focus on three key areas:
- Secure your workstation and CI environments to prevent initial compromise
- For application developers, implement safeguards against deployment vulnerabilities
- For library developers publishing to npm, establish protections to prevent package compromise
2.1. Avoid getting compromised packages
2.1.1. Favoring pnpm over npm
pnpm was developed with the idea of being a superior package manager, addressing pitfalls and common consequences of the rest, particularly focusing on disk space efficiency and speed. As a consequence, it excels in performance and is better maintained, reacting faster to the needs of the ecosystem.
Additionally, it has several features enabled by default, one that allows for a diff-friendly lockfile format, and others, including forbidding the execution of scripts (this makes me think we could’ve potentially prevented this whole mess, maybe?). Now it even allows you to delay dependency installations according to their release date.
2.1.2. Using pnpm to delay dependency updates
In most cases, malicious releases are discovered and removed from the registry within an hour — pnpm docsParting from the empirical statement that most attacks are usually discovered within their first days (I am not as optimistic as the pnpm team, as you can see) or at least for this kind of visible contexts, this update introducing minimumReleaseAge should help you decrease the chances of getting a compromised version.
ℹ️
minimumReleaseAge defines the minimum number of minutes that must pass after a version is published before pnpm will install it. This applies to all dependencies, including transitive ones.
Unfortunately, since this is a setting inside pnpm-workspace.yaml, it just ends up protecting only you, not the users who download your published package from npm.
There’s another interesting setting that I’ve found while reading ping-ponging with GPT-5 on pnpm’s documentation, which is resolutionMode. When used with the time-based option, direct dependencies will be resolved to their lowest version, and more importantly, subdependencies will be resolved from versions that were published before the last direct dependency was published.
2.1.3. Third-party security
There’s always the option to delegate this to organizations that do this for a living, of course. Two of which I curated links from this whole mess at the beginning, which can help you greatly. I’m speaking about Aikido and SocketDev. They even have free plans to get you started.
2.1.4. The dependency pinning fallacy
According to Perplexity: Version pinning is the practice of specifying and fixing the exact version or version range of software dependencies in a project. This is done to avoid unexpected issues caused by automatic updates and changes, ensuring consistent environments across development, testing, and production.I agree with most of these definitions, but it does not work like that in practice. In most package managers, transitive ranges leak through.
Let’s exemplify with axios. You think you froze a dep by pinning axios to 1.7.0. You didn’t. You froze the top-level package, but its children still float because their package.json uses ranges like ^ and ~. Those ranges keep pulling “compatible” updates whenever your lockfile moves.
"dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" }- Pin [email protected] all you want, you still get [email protected], [email protected], [email protected] at whatever the latest “compatible” is when you resolve.
- That means your build can change under your feet without you touching package.json.
- If one of those transitives ships a malicious “compatible” release, it will be updated. Your version pinning doesn’t save you from that.
Bottom line, pinning a top-level version pins the author’s dependency logic, not the whole tree. If you want determinism, you need to actually freeze the graph, not just the first hop. That’s what lockfiles are for.
2.1.5. Treating your lockfile with care
Thinking about this more, we would have to forbid deleting the package-lock.json and regenerating it with npm install and forbid the use of npm update so that package-lock.json would stay stable. — a user from the hackernews' thread.Your lockfiles have a snapshot of all deps your project uses, effectively pinning everything! Unfortunately, they are not published to the npm registry, so they don’t actually protect package consumers, nor force them to install many versions of the same dependencies. They’ll have to account for their own security.
How to use them carefully?
- Commit it to your repository, don’t ignore it!
- Don’t regenerate it unnecessarily:
- Do not delete it and regenerate it
- Do not modify dependencies in package.json manually
- Never trust lockfile modifications from external contributors
- Review and test any change to verify that it does indeed what you expect
2.1.6. Hardening your dev environment
When things fail, and they inevitably will, you may receive a compromised package. Using a hardened and somewhat isolated environment will prevent that from spreading to the rest of your system and processes across your organization.
One way you could do this is by using containers and devcontainers. It’s not infallible, but it is better than not taking any action at all. Be cautious when using untrusted extensions with VSCode-based IDEs.
A step closer to isolation —devcontainer-wizard
From the “Where you run your code?” series, here we bring you yet another article about devcontainers. This time, with the release of a new tool!
The Red Guildmatta
2.2. Publishing securely to the NPM registry
As a result of this incident, GitHub accelerated the release of a new integration with the NPM registry called Trusted Publishers. It was originally pioneered by PyPi two years ago, as a way to get API tokens out of build pipelines. In July of this year, it was also added to NPM.
2.2.1. Trusted Publishers
The key technology behind this is a protocol that allows NPM and GitHub Actions to negotiate temporary credentials, meaning that no NPM Access Token could be compromised this way.
This approach eliminates the security risks associated with long-lived write tokens, which can be compromised, accidentally exposed in logs, or require manual rotation. Instead, each publish uses short-lived, cryptographically-signed tokens that are specific to your workflow and cannot be extracted or reused. — npmjs docsHere’s a step-by-step guide on how to configure it. It essentially sets GitLab or GitHub as a Trusted Publisher for a specific organization, user, repository, workflow name (where OIDC permissions are specified), and environment-associated actions, if applicable.
ℹ️
Note: npm does not verify your trusted publisher configuration when you save it. Double-check that your repository, workflow filename, and other details are correct, as errors will only appear when you attempt to publish.
2.2.2. Implementing a secure workflow
The security of this process will be as strong as your workflow setup. If you run compromised code in the same workflow that performs the release, you may inadvertently help spread a worm.
Here’s a proposal to make it a little more secure. Have your workflow be split into three jobs — each job runs in an isolated environment:
- First job: builds the package with pnpm pack and uploads the artifact
- Second job: compares the generated artifact with its previous version of your package, so that you can detect any unexpected change
- Third job: publish the artifact generated in the First job
The Third job is the only privileged one, but it doesn’t install anything but node and npm — No dependency could steal the temporary token.
To ensure that this last job is privileged, we must use GH Environments, which must be declared in our Trusted Publishing setup.
By doing this, you will also get automatic provenance generation, so anyone can see for themselves how it was published! And by default!
Recognizing that this setup can be complex, Pato from NomicFoundation has helped me create an example for you to explore and learn how to implement a similar workflow:
GitHub - alcuadrado/trusted-publishing-example: An example package published using Github as a trusted publisher
An example package published using Github as a trusted publisher - alcuadrado/trusted-publishing-example
GitHubalcuadrado
2.2.3. A recap on the most immediate security measures
- Favor pnpm over npm
- Implement Trusted Publisher setup
- Navigate to your package's Settings → Publishing access
- Select Require two-factor authentication and disallow tokens
- Finally, click on Update Package Settings
- Strengthen publishing settings on accounts and organizations to require two-factor authentication (2FA) for all write and publishing actions
- When configuring two-factor authentication, use WebAuthn instead of TOTP
- Use deployment environments to add approval requirements
- Enable tag protection rules to control who can create release tags
- Regularly audit your trusted publisher configurations
- Remove any unused publish tokens from your npm account
If you're transitioning from token-based publishing:
- Set up trusted publishers first and verify they work
- Then restrict token access as described above
- Revoke any existing automation tokens that are no longer needed
Well, that was —probably— a lot of information to digest, particularly if you haven't heard about Trusted Publishing and provenance before. We hope this article provides the motivation you need to start improving your security, whether from a developer, maintainer, or simple consumer perspective.
In the meantime, I'll be supporting Pato in this GitHub issue that could be the beginning of something relevant to the npm ecosystem. Maybe.
See you at the next one!
.png)


