Securing Your JavaScript Dependencies: What Every Organisation Needs to Know

In November 2025, attackers compromised popular npm packages used by Zapier, PostHog, ENS Domains, and others—affecting over 25,000 repositories in a matter of hours. The attack didn’t exploit a bug. It exploited trust.
I’ll try to explain what happened, why traditional defences fall short, and what you can do—whether you consume packages or publish them.
Part 1: Understanding the Threat
So What Actually Happened?
Let me walk you through the attack, because it’s surprisingly simple once you see it.
Someone got hold of a maintainer’s npm credentials—probably through phishing, maybe credential reuse from a breach. Doesn’t really matter how. What matters is what happened next.
They logged in as that maintainer and published new versions of packages that thousands of organisations already trusted. Version 18.0.2 becomes 18.0.3. Looks completely normal. Your CI pipeline pulls the update automatically, runs npm install, and now malicious code is executing inside your build environment.
Within hours, secrets were being exfiltrated from thousands of pipelines. AWS keys, GitHub tokens, database credentials—all shipped off to attacker-controlled servers.
The clever bit? The packages still worked. Your tests still passed. Nothing looked wrong until someone noticed strange network traffic or unexplained GitHub activity.
Why Should You Care?
Here’s the thing about supply chain attacks: they bypass everything you’ve built to keep attackers out.
Your firewall? Irrelevant—the code is already inside. Your code reviews? You’re not reviewing npm packages line by line. Your vulnerability scanners? They look for known CVEs, not fresh malware.
When malicious code runs in your build pipeline, it has access to everything that pipeline can touch: cloud credentials, API keys, source code, potentially customer data. And because it’s running as part of your normal build process, it looks completely legitimate.
From a business perspective, you’re looking at potential regulatory issues (GDPR, SOC 2), reputational damage, and liability if customer data gets compromised through software you shipped.
“But We Pin Our Dependencies”
I hear this a lot. And yes, version pinning helps—it stops you from automatically pulling a malicious update. But it’s not the complete answer people think it is.
| What Pinning Prevents | What Pinning Cannot Prevent |
|---|---|
| Automatic upgrades to new malicious versions | Compromise of versions you’re already using |
| Dependency drift across environments | Attackers patching “security fixes” that look routine |
| Inconsistent builds | Social pressure to update (“critical security patch”) |
The real problem is this: when attackers control the publishing account, they control what “safe” looks like. They can publish a “security fix” that everyone rushes to adopt. They can wait until you eventually update. Pinning buys you time, but it doesn’t solve the underlying trust problem.
Part 2: What Can Organisations Do?
The Strategic View
If you’re in a leadership or risk role, here’s how I’d think about this.
First, accept that you can’t inspect every line of code in your dependency tree. A typical Node.js application has hundreds of transitive dependencies. You’re not going to audit them all, and neither is anyone else.
What you can do is reduce exposure and limit blast radius:
Be intentional about what you depend on. Every dependency is a trust relationship. Some packages are maintained by well-funded teams with security practices. Others are maintained by a single person in their spare time who might get phished next week. Know which is which for your critical dependencies.
Assume compromise will happen. Not “might”—”will.” Design your systems so that when a build pipeline gets compromised, the damage is contained. Rotate credentials regularly. Segment networks. Limit what secrets your CI/CD actually needs access to.
Have a response plan. If you found out tomorrow that one of your dependencies was compromised, do you know what you’d do? Which credentials would you rotate? How would you notify customers? Think through this before it happens.
For Development Teams
If you’re on a development team, here’s where I’d start:
Get visibility into what you actually have. How many direct dependencies? How many transitive? When I ask teams this, they often don’t know. Run npm ls or bun pm ls and look at the output. It’s usually eye-opening.
Question whether you need everything you’re using. That utility library you added three years ago for one function—do you still need it? Could you write those 20 lines yourself instead of trusting an external package? I’m not saying avoid all dependencies, but be thoughtful about the tradeoff.
Make updates deliberate, not automatic. Dependabot and similar tools are useful, but they can also accelerate adoption of compromised packages. Review what’s changing before you merge that PR. A few minutes of review is worth it.
Part 3: Technical Implementation for Package Consumers
CI/CD Hardening
The Shai-Hulud attack exploited lifecycle scripts—code that runs automatically during npm install. Your first line of defence is preventing this.
For npm:
# In CI/CD pipelines
npm ci --ignore-scripts
# In .npmrc (project-level)
ignore-scripts=true
For Bun:
Bun is more secure by default. It blocks lifecycle scripts unless explicitly trusted. However, you should still be explicit:
# Full lockdown
bun install --frozen-lockfile --ignore-scripts
# bunfig.toml
[install]
frozenLockfile = true
PIN GITHUB ACTIONS TO SHA, NOT TAGS
The same logic applies to GitHub Actions. When you write uses: actions/checkout@v4, you’re trusting that the v4 tag points to safe code. But tags can be moved. If an attacker compromises the action’s repository, they can point v4 at malicious code.
Pin to the commit SHA instead:
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
The comment preserves readability. The SHA ensures immutability.
The nice thing about this format is that Dependabot understands it. When a new version is released, Dependabot will open a PR that updates the SHA while preserving the comment format. You get security without losing visibility into what version you’re actually running.
I batch-convert all uses statements across workflows and composite actions using a script that supports dry-run and update modes. It’s tedious to do manually, but straightforward to automate.
LOCKFILE INTEGRITY
Your lockfile is your source of truth. Protect it.
# Validate lockfile hasn't been tampered with
npx lockfile-lint --path package-lock.json --allowed-hosts npm
# In CI: fail if lockfile would change
npm ci # (not npm install)
bun install --frozen-lockfile
NETWORK EGRESS CONTROLS
Malicious packages need to exfiltrate data. Block that path.
- Restrict build system network access to package registries only
- Monitor for unexpected outbound connections
- Use a caching proxy for npm to reduce direct registry access
TOOLING RECOMMENDATIONS
| Tool | Purpose | Notes |
|---|---|---|
| Socket.dev | Real-time malware detection | Integrates with GitHub, supports Bun |
| Snyk | Vulnerability scanning | Good CI/CD integration |
| npm audit | Built-in vulnerability check | Limited to known CVEs |
| Dependabot | Automated updates | Useful but creates update pressure |
Caution: Automated update tools can accelerate adoption of compromised packages. Balance automation with review processes.
Part 4: For Package Publishers
If you maintain npm packages, you’re a potential attack vector for everyone who depends on you. This section covers how to protect your users.
Account Security (The Primary Attack Vector)
Most supply chain attacks begin with account compromise. Harden yours.
Non-negotiable:
- Hardware security keys (YubiKey, etc.) for npm and GitHub—not TOTP
- Enforce 2FA for all publishing operations:
npm access 2fa-mode=auth-and-writes - Use a dedicated publishing account, separate from your daily-use account
- Audit collaborators regularly:
npm access ls—remove anyone who doesn’t need access
Why hardware keys matter: TOTP codes can be phished. Hardware keys cannot be remotely stolen.
Provenance and Trusted Publishing
Provenance attestations cryptographically link your published package to specific source code and build process. This lets consumers verify your package wasn’t tampered with.
npm Trusted Publishing (recommended):
As of late 2025, npm supports OIDC-based trusted publishing. This eliminates long-lived tokens entirely.
# .github/workflows/publish.yml
name: Publish
on:
release:
types: [created]
permissions:
contents: read
id-token: write # Required for OIDC
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- run: npm ci --ignore-scripts
- run: npm test
- run: npm publish --provenance
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
Configuration on npmjs.com:
- Navigate to your package settings
- Add a trusted publisher
- Specify: organisation/user, repository, workflow filename, environment
Once configured, npm automatically generates provenance attestations. The --provenance flag becomes optional.
Bun-Specific Guidance
Bun does not yet support bun publish --provenance natively. Use the npm CLI for publishing:
# Build with Bun, publish with npm
bun run build
bunx npm publish --provenance
Your package.json:
{
"name": "your-library",
"version": "1.0.0",
"publishConfig": {
"access": "public",
"provenance": true
},
"scripts": {
"prepublishOnly": "bun test && bun run build"
}
}
Pre-Publish Checklist
Before every release:
# 1. Check for vulnerabilities in your own dependencies
npm audit --audit-level=high
# 2. Review what you're actually publishing
npm pack --dry-run
# 3. Check for unexpected files
tar -tzf *.tgz | grep -E '\.(sh|exe|bin)$' && echo "WARNING: Unexpected executables"
# 4. Verify provenance will work
npm publish --dry-run --provenance
Help Your Users Verify
Add verification instructions to your README:
## Security
This package uses npm provenance. Verify the build origin at:
https://www.npmjs.com/package/YOUR-PACKAGE?activeTab=provenance
For maximum security, pin to exact versions:
npm install your-package@1.2.3 --save-exact
Part 5: Detection and Response
Indicators of Compromise
Watch for:
- Unexpected GitHub Actions workflows (especially
self-hostedrunners) - New repository collaborators you didn’t add
- npm publish notifications you didn’t trigger
- Outbound connections to
webhook.siteor similar services from CI - Files named
cloud.json,environment.json,truffleSecrets.jsonin your repos
If You Suspect Compromise
Immediate actions:
- Revoke all npm tokens:
npm token revoke - Rotate GitHub PATs and SSH keys
- Rotate cloud provider credentials
- Review recent npm publishes and unpublish if malicious
- Notify downstream users
Investigation:
- Review GitHub audit logs for PAT creation
- Check npm access logs
- Search for unauthorized workflows in
.github/workflows/
Summary: Priority Actions
If You Consume Packages
| Priority | Action | Effort |
|---|---|---|
| 1 | Disable lifecycle scripts in CI (--ignore-scripts) | Low |
| 2 | Use lockfiles with --frozen-lockfile | Low |
| 3 | Implement egress controls on build systems | Medium |
| 4 | Add dependency scanning (Socket, Snyk) | Medium |
| 5 | Establish dependency review process | Medium |
If You Publish Packages
| Priority | Action | Effort |
|---|---|---|
| 1 | Hardware MFA on npm and GitHub | Low |
| 2 | Configure Trusted Publishing (OIDC) | Medium |
| 3 | Publish with provenance attestations | Low |
| 4 | Audit and minimise collaborator access | Low |
| 5 | Document verification steps for users | Low |
