A man holding an npm sticker.

Malicious Compliance: How Trusted Packages Turn Into Attack Vectors

Niclas Hedam

PhD, Computer Science

· 12 min read · 4 sources · Corrected 5 July 2026

  • Installing one package means trusting its author, its contributors, and every maintainer down the dependency tree, and most of those trust decisions are made for you the moment you run the install command.
  • Supply chain attacks exploit that inherited trust through legitimate channels, such as phished maintainer accounts and routine updates, rather than obvious malware.
  • The risk cannot be eliminated, but it can be made deliberate: justify every dependency, pin versions, isolate builds, and monitor what packages actually do.

This website is built with Eleventy, a static site generator that pulls in several dozen packages the moment you install it. Two of them are iso-639-1, a small package that provides ISO 639-1 language codes, built and maintained by a single author in Singapore, and kleur, a popular package for colouring terminal output, maintained by a developer in the USA. I have never met either of them. I could not tell you what their security practices look like, or what happens to their npm account if their laptop gets stolen. And yet every time I rebuild this site, their code runs, unread, with the same privileges as my own.

Nobody sat down and decided this was an acceptable risk. There was no meeting, no checklist, no moment where I weighed trusting two strangers against writing my own language-code lookup table. The decision got made for me, by Eleventy’s dependency tree, the instant I typed npm install. That is the position most developers are in for most of their dependencies, most of the time, whether they stop to think about it or not.

This is the default state of modern software. Package managers like npm for JavaScript, pip for Python, and gem for Ruby have made code-sharing so easy that a typical web application now depends on hundreds, sometimes thousands, of packages, each solving one narrow problem and each dragging in its own dependencies behind it. The result is a tree, with your application at the root and a great many strangers holding up the branches.

The Convenience of Reuse

The convenience is undeniable. With a single command like npm install lodash, you get a mature, well-tested utility library that has been downloaded billions of times. The open-source model rewards collaboration, peer review, and fast iteration, and most days it delivers exactly what it promises.

The registries that host these libraries have quietly become critical infrastructure for the internet. They serve billions of downloads a day, pushing code straight into production systems worldwide with no human required to approve any single one. That scale is what makes modern development so fast. It is also what makes the tree worth attacking: compromise one widely-used package, and the automation that normally works in your favour starts working for the attacker instead.

Behind the Leaf

This is the part of the tree nobody looks at. When you install a package, you are not just trusting its author. You are trusting everyone who has contributed to it, everyone who maintains its dependencies, and everyone with access to publish an update, all the way down to the leaves. Neither iso-639-1 nor kleur ever gave me a reason to distrust them. But I did not choose them either. Eleventy did, on my behalf, the moment I installed it, and each of Eleventy’s dependencies made the same choice again for dependencies of their own.

The tree also keeps growing on its own. Every time a package somewhere in it publishes a new version, the next install can pull that version in without me touching anything. The set of strangers I am trusting today is not even the same set I trusted last month.

When a Leaf Rots

Whilst it may sound silly to be worried about a package that just provides language codes, or colours terminal output, the reality is that attackers have found ways to exploit these dependencies to compromise systems. This is known as a supply chain attack. And they happen all the time.

npm, the package manager for JavaScript, has been a frequent target. Large-scale supply chain attacks happen several times a year. In September 2025 alone, it happened twice. Up to 18 popular npm packages were compromised with malware as part of a supply chain attack. Packages were compromised through a phishing attack on the package maintainers and the compromised packages started downloading a cryptocurrency transaction interceptor that would silently steal cryptocurrency from users of popular wallets.

Breakdown: Widespread npm Supply Chain Attack Puts Billions of Weekly Downloads at Risk — A breakdown of the 8 September 2025 compromise, in which chalk, debug and around sixteen other packages were taken over through phished maintainer accounts and rigged to hijack cryptocurrency transactions in the browser.

Also in September 2025, a self-replicating worm appeared that steals GitHub developer credentials from affected systems to spread itself to other projects. As of 16 September, the worm had affected more than 187 packages, including ones published by the security firm CrowdStrike. That a company in the business of stopping this sort of thing had its own packages caught up in it says something about how hard the problem is.

187+ packages

Affected in two weeks, including packages published by CrowdStrike. The worm spread by stealing GitHub credentials from infected systems and using them to push malicious commits to other repositories.

Self-Replicating Worm Hits 180+ Software Packages — Reporting on the Shai-Hulud worm, which spread across npm by stealing developer credentials and reached at least 187 packages, including ones published by CrowdStrike.

Package Managers, Malicious Compliance, and You

The root cause of these attacks is the very convenience that package managers provide. They make it easy to install and update packages, but they also make it easy to introduce malicious code. When you run npm install, you are executing code from the package’s postinstall script, which can do anything. If an attacker can compromise a package, they can execute arbitrary code on your system.

Notice that nothing malfunctions when this happens. The package manager does exactly what it was designed to do: fetch the newest version and run its install scripts, no questions asked. You asked for the latest code, and the latest code is now malicious. There is no break-in to notice, because the package manager is only doing its job. That is what makes these attacks so hard to catch.

Running npm install executes any postinstall scripts in newly downloaded packages with the same privileges as your own code without asking for permission. A compromised package can exfiltrate credentials, establish persistence, or modify local files silently. The install step is not just downloading files; it is executing untrusted code.

It gets worse, because popularity is treated as a proxy for safety. A package downloaded millions of times feels trustworthy, but the download count says nothing about whether the maintainer’s account was phished last week. If anything, popularity makes a package a more attractive target, not a safer one.

Antivirus Is Not Enough

Traditional security measures like antivirus software are not sufficient to protect against supply chain attacks. Antivirus software relies on known signatures to detect malware, for example through hashing. However, since npm packages are frequently updated and can even be built locally, the hash of a package can differ. This means that even if a package was previously known to be safe, a new version could introduce malicious code that goes undetected by antivirus software.

Yarn, a popular alternative to npm, has introduced auditing features that can help identify known vulnerabilities in packages. However, these features are not foolproof and can only detect known issues. They cannot protect against zero-day vulnerabilities or sophisticated attacks that exploit trust relationships.

What You Can Actually Do

None of this means writing everything yourself. It means treating a dependency as something you take on deliberately, rather than something that arrives by default. Some teams make that explicit with a dependency budget, the same way they keep a performance budget: a limit that forces a real decision about whether the next package is worth the strangers who arrive with it. The practices below are the ones I actually use, and none of them are exotic.

Start by watching what you already have. Tools like Snyk and Dependabot keep an eye on your dependency tree for known vulnerabilities and open pull requests when a fix exists. They will not catch a fresh compromise, since they only know about problems that have already been reported, but they stop you carrying old, well-documented holes for no reason.

Then install and build in something you can throw away, such as a container or a virtual machine rather than your main computer. If a package does turn out to be hostile, the damage is trapped in a box you can delete. The same logic applies to deployment: keep your build and release environments separate from where you work, and let a new build sit in a canary environment before it reaches everyone, so anything strange has a chance to surface before it is in front of users.

For a small package, reading the source before you depend on it is often an afternoon rather than a research project, and small packages are exactly the ones nobody bothers to read. For the rest, look at the people. Is the project actively maintained? Do the maintainers answer their issues? The question is the same one I could ask about iso-639-1 and kleur: would you hand this person the keys to your production system? Because installing their package does exactly that.

Pin your versions with a lock file (package-lock.json, yarn.lock, Pipfile.lock) so an update is something you choose and test, not something that happens overnight because a dependency three levels down published a new release. This is what closes the gap I described earlier, where the tree quietly changes between installs.

Finally, watch the packages once they are running. A language-code library has no reason to open a network connection, and a colour library has no reason to touch your files. If one of them suddenly does, you want an alert, not silence. Log the unusual, and flag it when a new dependency shows up so that someone actually looks at it.

The Human Element

Strip away the tooling and every one of these attacks comes down to a person. Open-source software is built by people with different levels of security awareness, different motivations, and different circumstances. A maintainer might be overwhelmed, burnt out, or simply make a mistake. They might be phished, or have an account compromised through no fault of their own.

And because the registries are centralised, one compromised person becomes everyone’s problem. The 2021 attack on the Codecov bash uploader reached the CI pipelines of a great many companies, including major technology firms. The 2022 compromise of the node-ipc package by its own maintainer showed the other version of the same risk: even a trusted author can decide to become the threat.

Codecov (2021): Attackers injected a script into Codecov’s bash uploader by exploiting a misconfiguration in its build pipeline. The script silently exfiltrated environment variables, including CI/CD secrets and API keys, from any project that ran the uploader. It ran undetected for roughly two months before a customer noticed the tampering.

node-ipc (2022): The maintainer of node-ipc, a widely-used package with more than a million weekly downloads, deliberately introduced destructive code targeting users in Russia and Belarus. Files were overwritten with a heart symbol. This was not an external attack, but rather a trusted author making a political statement through a dependency used by millions.

Bash Uploader Security Update — Codecov’s own disclosure of the 2021 compromise, in which its Bash Uploader was altered to exfiltrate environment variables, including CI credentials and tokens, from the pipelines that ran it.

Popular NPM Package Updated to Wipe Russia, Belarus Systems to Protest Ukraine Invasion — A report on the node-ipc maintainer shipping code that wiped files on machines whose IP address placed them in Russia and Belarus, overwriting their contents with a heart emoji.

The Industry Is Catching Up

The industry is slowly catching up. Initiatives like the System Package Data Exchange and Supply-chain Levels for Software Artifacts are trying to make it possible to say what is actually inside a piece of software and where each part came from, and registries like npm now require two-factor authentication from the maintainers of their most-used packages. These are real improvements.

But they arrive slowly, and none of them change the position you are in today. Every attack in this post happened after the industry already knew that supply chains were the weak point. Until the fixes are everywhere, the caution has to come from you.

Trust as a Decision

I still do not know much about the people who maintain iso-639-1 or kleur. A name, a country, a download count. That is the entire trust relationship, and it has worked fine for years, right up until the week it does not.

None of this makes open source a mistake. Ripping every dependency out of this site would mean writing my own static site generator, and that trade is not worth it. The point is smaller and more practical: the trust should be a decision, not a default. Read the packages you can. Pin the versions you use. Isolate the ones you cannot fully vet. Watch what they do once they are running. None of that removes the risk. It just means that when a maintainer’s account gets phished, or a maintainer decides to make a political statement through your production system, you find out from your own monitoring rather than from the news.

The convenience is real, and I am not going to pretend I would give it up. But the names I am trusting to keep this website running deserve more than a download count, and so does every other stranger whose code is quietly running on your machine right now.

  • Rewrote the introduction and closing section around this site's own dependency tree, reworked the recommendations from a checklist into prose, added source citations throughout, and corrected the node-ipc download figure and a few incident details to match the cited sources.

The views and perspectives expressed here are the author's own and do not represent any employer or affiliated organisation. The writing draws on public sources and the author's own experience, never on confidential information. Artificial intelligence is used on some posts to identify sources, draft structure, and assist with quality assurance; the final article is always the author's own work. The AI assists, but never authors.

Niclas Hedam

PhD, Computer Science

Niclas Hedam holds a PhD in Computer Science from the IT University of Copenhagen. He is passionate about educating others on the importance of safeguarding personal information online.

A developer using ChatGPT to generate code snippets.

LLMs Can Write Code, but Cannot Read Your Mind

· 11 min read · 4 sources

LLMs generate valid code fast, but they cannot tell a secure pattern from an insecure one that looks identical, because they do not know what your code is for.