You type npm install or pip install dozens of times a week without a second thought. Each of those commands downloads code written by people you have never met and lets it run on your laptop, your build server and often your production machines. Attackers have figured out that poisoning that borrowed code is far easier than breaking into a hardened app directly.

The last couple of years made this painfully clear. Attackers hijacked the axios maintainer account and shipped remote access trojans to a library with tens of millions of weekly downloads. Phishing emails handed over the npm accounts behind chalk and debug. A self-spreading worm called Shai-Hulud infected hundreds of packages. A compromised Trivy scanner workflow cascaded into Checkmarx KICS and LiteLLM. None of these victims did anything unusual. They simply trusted their dependencies.
This guide breaks down how these attacks work and gives you a practical defense plan you can apply whether you run a solo side project or a large engineering team.
What Is a Software Supply Chain Attack?
A normal attack targets your application. A supply chain attack targets something your application depends on: a package, a GitHub Action, a container image, an IDE extension, a developer's laptop or a CI runner. Once the attacker controls that piece, your own trust settings carry the malicious code straight into your environment.
That trust is the whole problem. Your package manager installs whatever the registry serves. Your pipeline runs whatever action a tag points to. Your build machine hands secrets to whatever script asks for them. Attackers exploit the gap between what you assume is safe and what you actually verified.
6 Common Supply Chain Attack Methods Developers Face
Stolen maintainer accounts
The attacker phishes or steals a maintainer's registry credentials and publishes a new version through the official channel. The package name, publisher and signature all look legitimate, so automated checks pass. In the axios case, the bad versions stayed live for only about three hours, and that window still reached a huge number of installs.
Look-alike and hallucinated package names
Typosquatting relies on simple mistakes like typing axois instead of axios. Dependency confusion abuses the way package managers choose between private and public registries. A newer twist, often called slopsquatting, targets AI coding assistants. Models sometimes invent package names that sound real. Attackers register those invented names, fill them with malware and wait for an assistant to suggest them.
Install scripts that run before you check anything
npm lifecycle hooks such as preinstall and postinstall execute code automatically during installation. Recent worms leaned on pre-install hooks, and some even used the Bun runtime to dodge tooling built around Node. If your security scan runs after installation, it inspects a machine the malware already owns.
Poisoned CI/CD pipelines
Build systems hold the most valuable secrets you have: publishing tokens, cloud keys and signing credentials. A misconfigured pull_request_target workflow in Trivy leaked secrets, and the attackers used them to rewrite release tags and spread further. The tj-actions/changed-files compromise pulled malicious code into thousands of pipelines because those pipelines referenced the action by a movable tag.
Long-game social engineering
The XZ Utils backdoor showed how patient attackers can be. Someone spent roughly two years contributing helpful code, earned maintainer rights and then slipped in a backdoor. An engineer caught it only because SSH logins suddenly used more CPU than they should.
Compromised AI agents and tools
Coding agents now install packages, edit configs and push commits on their own. MCP servers connect those agents to internal systems with broad permissions. A malicious agent skill or MCP server can quietly alter commits or leak data, and the developer may never notice.
Why Supply Chain Attacks Keep Working
The math favors the attacker. Breaking into one well-defended company takes research, patience and luck. Breaking into one popular package, one build tool or one maintainer account gives access to thousands of companies for roughly the same effort. The cost per victim drops close to zero.
Trust also spreads through your stack without anyone deciding it should. A signed update runs. An allowlisted vendor connects. A package from a familiar publisher installs. Teams built these shortcuts for convenience, and attackers now use that convenience against them.
Size offers no protection either. A small team that uses a compromised library has the same exposure as a giant enterprise, and it often has fewer environment boundaries and less monitoring to limit the damage.
How Self-Spreading npm Worms Like Shai-Hulud Work
Older malicious packages hit whoever installed them and stopped there. Worms like Shai-Hulud keep going. After landing on a machine, they sweep environment variables, dotfiles and cloud configs for secrets. Some even run TruffleHog, a defender's secret scanner, to find credentials faster and then test which keys still work.
The worm then uses any npm token it finds to publish infected versions of every package the victim maintains. Stolen secrets often get dumped into public GitHub repositories, because traffic to GitHub rarely raises alarms. Some variants planted GitHub Actions workflows that execute commands posted in repository discussions, giving the attacker a way back in later.
Each new victim becomes a launch pad for the next. That loop explains why one campaign can jump from a few packages to hundreds within days.
How to Secure Your npm and PyPI Dependencies
Start here. These controls cost nothing except setup time.
Pin exact versions. Caret and tilde ranges like ^1.14.0 pull new releases automatically, including malicious ones. Specify exact versions and commit your lockfile.
// risky: grabs any new 1.x release
"axios": "^1.14.0"
// safer: changes only when you decide
"axios": "1.14.0"Install from the lockfile in CI. Use npm ci instead of npm install, and pip install --require-hashes for Python. These commands refuse anything that does not match what you already approved.
Add a cooldown before adopting new releases. Most malicious versions get spotted and pulled within hours. A waiting period of three days to a week wipes out most of that risk. Renovate and Dependabot both support a minimum release age.
{
"extends": ["config:recommended"],
"packageRules": [
{ "matchUpdateTypes": ["minor", "patch"], "minimumReleaseAge": "3 days" },
{ "matchUpdateTypes": ["major"], "minimumReleaseAge": "7 days" }
]
}Fast-track only the updates that fix an actively exploited vulnerability.
Turn off install scripts by default. pnpm already blocks post-install scripts and lets you allow specific packages. On npm, add this to .npmrc:
ignore-scripts=trueWhen you need to scan a new dependency, install it with scripts disabled, run your scan, then reinstall with scripts enabled only if it passes.
Cut dependencies you do not need. Every package adds attack surface, and transitive dependencies multiply it. Run npx depcheck to find unused packages. Before adding a new library, ask whether a few dozen lines of your own code would do the job.
Route installs through a registry you control. A proxy such as Verdaccio, Nexus, Artifactory or AWS CodeArtifact sits between your team and the public registry. You can scan, delay and approve versions there before any developer or pipeline sees them.
How to Protect Maintainer Accounts, Tokens and Secrets
Use phishing-resistant MFA. Hardware keys based on FIDO2 or passkeys stop the credential phishing that opened the door for chalk, debug and many others. Enable MFA on every registry account your team touches: npm, PyPI, RubyGems, Docker Hub and GitHub.
Prefer short-lived credentials. OIDC tokens from GitHub Actions expire in about an hour, so a thief gets very little time to use them. Long-lived personal access tokens can sit in a stolen dump for months. The Trivy breach traced back to a single broad token reused across more than 30 workflows.
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/deploy
aws-region: us-east-1Give every token the smallest scope possible. No single token should reach across repositories or the whole organization. Remove access for former contributors right away.
Clean up cleartext secrets on developer machines. Check ~/.ssh/, ~/.aws/credentials, ~/.docker/config.json, .env files and shell history. Move secrets into a password manager or vault, and add a passphrase to SSH keys. On Linux, avoid Git's plain-text store credential helper.
Plant canary tokens. Fake AWS keys or API tokens placed on laptops and runners do nothing except trigger an alert when someone uses them. If an infostealer grabs them, you hear about it before the attacker touches anything real.
How to Secure CI/CD Pipelines and GitHub Actions
Pin actions and images to immutable references. Tags can move. Commit SHAs and image digests cannot.
# a tag can be force-pushed to malicious code
- uses: some-org/some-action@v1
# a full commit SHA stays fixed
- uses: some-org/some-action@8f4b7f84bd579b95d7f0b90f8d8b6e5d9b8a1c2eKeep an allowlist of approved third-party actions across your organization, and turn on immutable releases for actions you maintain.
Treat pull_request_target with extreme care. It runs with the base repository's secrets. Avoid it unless you truly need it, and never check out untrusted pull request code inside it.
Run each job in a fresh, disposable environment. Ephemeral runners give a backdoor nowhere to persist. Restrict outbound network access so a compromised step cannot ship your secrets to an unknown server.
Separate secrets by step. A linting or scanning step should never see deployment credentials. Test dependency updates in an environment that holds no production secrets at all.
Pin your security tools too. The Trivy incident proved that scanners themselves can carry malware. Lock their versions like any other dependency.
How to Use SBOMs and Package Provenance to Verify Software
Generate an SBOM for every build. A software bill of materials lists every component inside a release. When the next compromise hits, you can answer "are we affected?" in minutes instead of digging through running containers. Syft and CycloneDX make this simple.
syft dir:. -o cyclonedx-json > sbom.json
syft myapp:latest -o spdx-json > sbom.spdx.jsonPublish with provenance and check signatures. Running npm publish --provenance links a package to the exact source and build that produced it. Running npm audit signatures checks the packages you install. For internal artifacts, Sigstore offers keyless signing.
Watch for odd changes in packages you rely on. A new, unknown dependency appearing in a patch release is a classic warning sign. The malicious axios versions pulled in a never-before-seen package that dropped the trojan. Behavior-based scanners flag new network calls, file access and obfuscated code that a CVE database will not catch.
How to Secure AI Coding Agents and MCP Servers
Sandbox your coding agents. Run them inside a container or microVM with its own filesystem and deny-by-default network rules. If an agent installs a bad package, the damage stays inside the box.
Treat MCP servers as dependencies. Verify the publisher, pin the version, limit permissions and log their traffic. Many MCP servers run with far more access than their task needs.
Give agents a way to check packages before installing them. This curbs both hallucinated package names and the habit of pulling outdated library versions.
Guard against prompt injection in pipelines. Once an agent reads issue titles, PR descriptions or comments into its prompt, any outside contributor can feed it instructions. Keep agent permissions in CI as narrow as possible.
Review code carefully, especially big diffs. GitHub collapses large diffs by default, and malicious payloads love to hide there. Require signed commits and branch protection so nobody can rewrite history quietly.
What to Do If a Malicious Package Compromises Your Project
Speed matters, but panic causes its own damage. Work through these steps in order.
- Find every affected system. Query your SBOMs and device management tools to see which machines and projects contain the bad package version.
- Contain the spread. Pin back to the last known good version, pause affected pipelines and block outbound traffic from infected hosts.
- Rotate everything. Assume every secret on an infected machine or runner leaked. Revoke registry tokens, cloud keys, SSH keys and CI secrets.
- Hunt for persistence. Look for rogue GitHub workflows, new public repositories, suspicious discussions, scheduled tasks and leftover files. Some Python malware survived package removal through
.pthfiles. - Check the audit logs. GitHub keeps about 90 days of activity, and cloud audit trails such as AWS CloudTrail reveal what stolen keys actually did.
- Protect your users. Unpublish infected package versions, pull bad app builds from stores and warn users through your most visible channel.
- Distrust quick community fixes. Attackers love publishing fake "cleanup" packages during an incident. Verify anything before you run it.
Write this runbook before you need it, and walk through it once with your team so everyone knows their role.
Supply Chain Security Checklist for Developers
- Pin exact dependency versions and commit lockfiles
- Use
npm cior hash-checked installs in CI - Set a three-day minimum release age for routine updates
- Disable install scripts by default
- Turn on hardware-key MFA for every registry and GitHub account
- Replace long-lived tokens with OIDC where possible
- Pin GitHub Actions to commit SHAs and images to digests
- Run CI jobs in ephemeral, network-restricted runners
- Generate SBOMs for every release
- Sandbox AI agents and vet every MCP server
- Remove cleartext secrets from developer machines and add canary tokens
- Keep a written incident runbook ready
Supply chain attacks now show up as a regular part of software development, not a rare headline. You cannot stop attackers from targeting the packages you use, but you control your lockfile, your token scopes, your pipeline config and the machines your team works on. Pin and delay your dependencies, shrink and shorten the life of your secrets, isolate your builds and agents, and plan your response in advance. Teams that do these basics turn a potential disaster into a quick cleanup.
Frequently Asked Questions
What is the fastest way to reduce my risk?
Pin exact dependency versions, commit your lockfile and add a three-day wait before adopting new releases. These three changes cost nothing and block most short-lived malicious versions.
Should I disable npm install scripts?
Yes, for most projects. Set ignore-scripts=true in .npmrc or switch to pnpm, then allow scripts only for the few packages that truly need them.
Does MFA stop these attacks?
It stops the credential theft behind many of them, especially when you use hardware keys or passkeys. It does not stop attacks that abuse CI workflows or cache poisoning, so pair it with scoped, short-lived tokens and pinned actions.
Can small teams and solo developers be targets?
Yes. Attackers do not check your revenue before a compromised package runs on your machine. Solo developers often hold publishing tokens and cloud keys on one laptop, which makes protecting that laptop a priority.





