Safeguard
Software Supply Chain Security

Preventing malicious packages with automated detection

Malicious npm and PyPI packages skip CVEs entirely. Here's how attackers get them published and how automated detection catches them before they ship.

Safeguard Research Team
Research
6 min read

On December 2, 2024, someone published two new versions of @solana/web3.js — 1.95.6 and 1.95.7 — to npm. The package had roughly 3.5 million weekly downloads at the time. Both versions contained an addToQueue function that quietly exfiltrated private keys from wallets that imported the library, funneling funds to an attacker-controlled address. The compromise was live for about five hours before maintainers pulled the versions, but that was enough time for several projects to lose funds. No CVE existed when the packages shipped. No signature-based scanner flagged them. The code simply looked like a library update, because it was one — except for the thirty lines quietly siphoning secrets.

This is the pattern behind most malicious package incidents: legitimate publishing infrastructure, a compromised or coerced maintainer, and a payload that traditional vulnerability scanning was never built to catch. Below is what the data actually shows about how these attacks happen and what automated detection needs to do differently to stop them before they reach a build.

What actually counts as a malicious package?

A malicious package is one that was intentionally written or modified to harm the systems that install it, as opposed to a package with an accidental security flaw. That distinction matters because it changes the detection strategy entirely. A vulnerable package (say, a Log4j build with CVE-2021-44228) has a flaw that was published unintentionally and gets fixed once discovered. A malicious package is built to work exactly as its author intended — the "bug" is the feature. Researchers generally bucket these into four categories: typosquats (reqeusts instead of requests), dependency confusion (an internal package name claimed on a public registry before a company publishes it), account/maintainer takeovers (a legitimate package hijacked via stolen credentials or social engineering), and protestware or intentional sabotage baked in by an original maintainer. The node-ipc incident in March 2022 is the clearest example of the fourth category: maintainer RIAEvangelist shipped a "peacenotwar" module into a package with roughly 1.1 million weekly downloads that overwrote files on machines with Russian or Belarusian IP addresses.

How do attackers actually get malicious code into public registries?

They exploit the trust relationship between a package name and its maintainer, not a technical flaw in the registry itself. The ua-parser-js incident in October 2021 is the template: an attacker gained access to a maintainer's npm account and pushed versions 0.7.29, 0.8.0, and 1.0.0 laced with a cryptominer and a password-stealing trojan to a library with over 7 million weekly downloads. Three years later, the XZ Utils backdoor (CVE-2024-3094), discovered on March 29, 2024 by Andres Freund at Microsoft, showed a more patient version of the same tactic — a contributor identity ("Jia Tan") spent roughly two years building trust and commit history before slipping a backdoor into liblzma that targeted OpenSSH connections on glibc-based Linux systems. The 2018 event-stream compromise, where a malicious actor got publish rights to a popular package and added a dependency called flatmap-stream designed to steal Bitcoin from the Copay wallet app, established that this playbook works against downstream targets several dependency layers removed from the attacker's actual point of entry.

Why do traditional SCA tools miss most malicious packages?

Traditional software composition analysis tools match installed packages against databases of known, disclosed vulnerabilities, and a brand-new malicious package has no CVE to match against. SCA was built to answer "is this dependency version affected by a disclosed flaw," which is a fundamentally different question from "does this code do something it shouldn't." Sonatype's research team, which crawls npm, PyPI, and other registries directly, reported identifying more than 512,000 malicious packages in 2024 alone — a jump from roughly 245,000 the year before. Almost none of those ever received a CVE identifier, because CVEs are assigned to vulnerabilities in legitimate code, not to packages that are malicious by design. A CVE-matching tool can be fully up to date and still install a credential stealer the same day it's published, because the detection logic is looking in a database that hasn't caught up yet — and for most malicious packages, never will.

How does automated malicious package detection actually work?

Automated detection works by analyzing what a package's code and install scripts actually do, rather than checking its version number against a list. That typically combines several layers: static analysis of package source for known malicious patterns (obfuscated code, base64-encoded payloads, calls to eval paired with network requests), behavioral sandboxing that runs preinstall/postinstall scripts in isolation to see if they phone home, exfiltrate environment variables, or write to unexpected paths, and diffing against a package's publishing history to flag when a new version suddenly adds network calls or file-system access a prior version never had. Socket and Phylum's registry-monitoring research has repeatedly shown that most malicious npm and PyPI packages get caught within hours of publication when this kind of pipeline runs continuously against registry firehoses, rather than waiting for a security researcher to notice and file a report — which is closer to the multi-day-to-multi-week timeline typical of CVE-driven discovery.

What should happen after a malicious package is detected in your environment?

The response has to be automatic, because the exposure window is measured in hours, not days. When a scanner confirms a package is malicious, the immediate steps are: block the version from being installed anywhere it isn't already present, alert every team with it in a manifest or lockfile, and open a fix — automatically, if possible — that pins to the last known-clean version or an alternative package. Manual triage doesn't scale to this problem: the @solana/web3.js compromise was resolved in about five hours precisely because the ecosystem (registry maintainers, downstream scanners, and the project team) moved fast, and organizations still running nightly or weekly dependency scans during that window had no chance of catching it before it shipped to production.

How Safeguard Helps

Safeguard detects malicious packages at publish time by combining registry-firehose monitoring with Griffin AI, which analyzes package behavior, install scripts, and code diffs against prior versions to flag intent-driven threats that have no CVE to match against. Reachability analysis then tells you whether a flagged package's malicious code path is actually invoked by your application, so security teams can triage the handful of exploitable hits instead of chasing every dependency in a lockfile. Safeguard generates and ingests SBOMs across your build pipeline, giving you a live inventory of every package version in use the moment a threat is confirmed, rather than a paper record from your last audit. When a malicious or vulnerable package is confirmed, Safeguard opens an auto-fix pull request that pins or replaces the affected dependency, cutting the time between detection and remediation from days to minutes.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.