# Malicious Package

> A malicious package is a registry library containing malicious code that steals data, opens a backdoor, or destroys files when installed, built, or imported.

Also known as: malicious code, malicious dependency, malicious npm package, malicious PyPI package, package malware, trojanized package, open source malware, embedded malicious code, CWE-506

Source: https://socket.dev/glossary/malicious-package

Last updated: 2026-10-15

**A malicious package**, also called a malicious dependency, is a library published to a public registry such as npm or PyPI that contains malicious code: code written to steal credentials, open a backdoor, or destroy data on the machines that install, build, or import it. It runs with the installer's privileges, usually before anyone reads it.

A package manager is a code execution service that developers invoke on purpose. Installing one dependency can run its code at seven different points, from an npm `postinstall` hook to a Python `.pth` file that fires on every interpreter start, and most of them fire before your application executes a single line. The OpenSSF Malicious Packages dataset held [239,136 malicious-package reports](https://ossf.github.io/malicious-packages/stats/) as of October 15, 2026, 93% of them for npm, though one spam wave accounts for 59% of the total. Our position is that malicious code is not a kind of vulnerability. It rarely gets a CVE, reachability does not apply, and the decisive control is analyzing what a package actually contains before it is installed.

## What is malicious code?

Malicious code is code whose purpose is to harm the system that runs it or the people who rely on that system. The difference from a bug is intent. A vulnerability is a mistake an attacker can exploit; malicious code is the attacker's own work, shipped to run. MITRE catalogs it as [CWE-506, Embedded Malicious Code](https://cwe.mitre.org/data/definitions/506.html): "code that appears to be malicious in nature."

Malicious code and [malware](https://socket.dev/glossary/malware) overlap but are not the same. Malware usually names a whole program: a trojan, a ransomware binary, a cryptominer. Malicious code also covers one injected line inside an otherwise legitimate library, which is what most software supply chain attacks look like. This page covers the general concept and its most common delivery vehicle today, the malicious package.

Intent is hard to observe from outside, so the working definition is behavioral. The [OpenSSF Malicious Packages project](https://github.com/ossf/malicious-packages), which maintains the largest open dataset, counts a package as malicious when installing or using it "would require some sort of incident response" because confidentiality, integrity, or availability was lost. That test settles most of the gray areas:

| Case | Malicious under the OpenSSF definition? |
| --- | --- |
| Typosquat with no payload | No, but reports are accepted because it is hard to tell apart from dependency confusion |
| Dependency confusion package, even an empty one | Yes. OpenSSF treats it like an account takeover |
| Protestware that only prints a message | No |
| Protestware that deletes files or degrades availability | Yes |
| Install-time telemetry | No, unless it exfiltrates sensitive data or provides remote access |
| Obfuscated code | Not by itself, but reports are accepted on registries that ban obfuscation, such as PyPI and crates.io |
| Offensive security tool | No, unless it executes a payload on install |

We agree with the behavioral test, but we apply it earlier. OpenSSF asks whether a victim would need incident response. We ask whether the package's code can cause that outcome, and we ask before anyone installs it.

## What is a malicious package?

A malicious package is malicious code packaged in the format a package manager already trusts: a name, a version, a manifest, a README, and files the installer will fetch and execute on request. Attackers get one onto a developer's machine in three ways. They publish a new package under a name people will install by mistake, they put code into a package people already use, or a maintainer turns on their own users.

New attacker-owned packages depend on naming. [Typosquatting](https://socket.dev/glossary/typosquatting) registers a name one typo away from a popular one. [Dependency confusion](https://socket.dev/glossary/dependency-confusion) publishes a public package with the same name as a company's internal one, so the resolver picks the public copy. [Slopsquatting](https://socket.dev/glossary/slopsquatting) registers names that AI coding assistants hallucinate. All three start with no reputation, so they copy the real project's README, author field, and repository link to borrow one.

Compromised legitimate packages are more dangerous because they inherit trust and traffic. An attacker who completes a [maintainer account takeover](https://socket.dev/glossary/maintainer-account-takeover) can publish a version that every caret range in the ecosystem accepts automatically. [The axios compromise](https://socket.dev/blog/axios-npm-package-compromised) in March 2026 changed almost nothing in axios itself. It added one new dependency, `plain-crypto-js`, to a library with 100 million weekly downloads, so the payload arrived as a [transitive dependency](https://socket.dev/glossary/transitive-dependency) that no one had chosen. [Shai-Hulud](https://socket.dev/blog/ongoing-supply-chain-attack-targets-crowdstrike-npm-packages) went one step further and turned each stolen npm token into the next compromise, which makes it a [worm](https://socket.dev/glossary/worm).

The third path needs no attacker at all. [Protestware](https://socket.dev/glossary/protestware) is code a legitimate maintainer adds to make a point, and some of it destroys data. Registries are also not the only channel. Editor extensions, MCP servers, and agent skills are packages too. The [GlassWorm](https://socket.dev/supply-chain-attacks/glassworm-v2) campaign [delivered malware through Open VSX extensions](https://socket.dev/blog/open-vsx-transitive-glassworm-campaign) that later declared other extensions as dependencies, so the editor installed the malicious ones automatically.

## When does a malicious package's code run?

A malicious package's code can run at seven points between `install` and production, and the new install-script defaults cover two of them. Package managers were designed to make installation do work: compile native bindings, download platform binaries, generate files. Each of those conveniences is a hook where third-party code executes with the installer's privileges.

> Seven points where a dependency's code runs. npm 12 install-script defaults cover only the first two.

The figure lists the seven execution points with a real case for each. The same data, with the trigger and the default state as of October 2026:

| # | Execution point | Triggered by | Real case | Off by default? |
| --- | --- | --- | --- | --- |
| 1 | npm lifecycle scripts (preinstall, install, postinstall) | npm install | plain-crypto-js 4.2.1, the axios compromise, March 2026 | Yes in npm 12 and pnpm 10 |
| 2 | Implicit native build from a binding.gyp file | npm install | Miasma "Phantom Gyp" waves, 2026 | Yes in npm 12 |
| 3 | Python source distribution build (setup.py or build backend) | pip install, pip download, pip install --dry-run | crytic-compilers 0.3.11, June 2024 | No. pip install --only-binary :all: avoids it |
| 4 | Build scripts | cargo build runs build.rs | proc-macro1, pulled in by arrayref 0.3.10, August 2026 | No |
| 5 | Interpreter startup hook | Any python start reads \*.pth | Mini Shai-Hulud PyPI wheels, June 2026 | No |
| 6 | Import-time code | import telnyx runs \_client.py | telnyx 4.87.1 and 4.87.2, March 2026 | No |
| 7 | Deferred runtime trigger | A date, a call count, or a remote signal | shanhai666 NuGet packages, armed for 2027 and 2028 | No |

Install time is still where most attacks execute. When [npm 12](https://socket.dev/blog/npm-12) shipped, we noted that "almost every worm and credential stealer that hit the registry since late 2025 ran at install time." npm 12 made dependency lifecycle scripts opt-in on July 8, 2026, including the implicit `node-gyp rebuild` that the [Miasma "Phantom Gyp" waves](https://socket.dev/supply-chain-attacks/miasma-mini-shai-hulud-supply-chain-attack) used to run code with no script in `package.json`. [pnpm 10](https://socket.dev/blog/pnpm-10-0-0-blocks-lifecycle-scripts-by-default) did the same for lifecycle scripts in January 2025. Default-on install scripts were a design mistake, and both changes were overdue. pnpm contributor Brandon Cheng put it plainly in the pull request: "It's nuts that a \[pnpm\|yarn\|npm\] install can run arbitrary code in the first place."

Attackers are already moving down the list. The [telnyx compromise](https://socket.dev/blog/telnyx-python-sdk-compromised) put its payload in the SDK's core HTTP client so it fired on `import telnyx`, and our analysis concluded that "by moving execution into the library's import path, the threat actor sidesteps that entire class of detection." The [compromised Rust crates](https://socket.dev/blog/popular-rust-crates-compromised) added a typosquatted dependency whose `build.rs` ran during any Cargo build that resolved it, so the application never had to call it. Blocking install scripts shrinks the attack surface. The malicious code itself is unchanged, and it waits for the next trigger.

### A wheel that runs nothing at install and runs on every Python start

Python's `site` module executes every line in a `.pth` file that begins with `import`, each time an interpreter starts. Installing a wheel runs no package code, which makes wheels look safe. A `.pth` file inside the wheel moves execution to the next start of any Python process in that environment, for any program. The [Mini Shai-Hulud](https://socket.dev/supply-chain-attacks/mini-shai-hulud) campaign's [PyPI wheels used exactly this](https://socket.dev/blog/mini-shai-hulud-miasma-and-hades-worms-target-bioinformatics-and-mcp-developers-via-malicious) in June 2026. We built a harmless wheel to show the behavior, using Python 3.13.16 and pip 26.2.1 on October 15, 2026:

```text
# innocent_utils-1.0.0-py3-none-any.whl
innocent_utils/__init__.py          def add(a, b): return a + b
innocent_utils-setup.pth            import os; open(f"/tmp/pth-ran-{os.getpid()}", "w").write("pwned\n")
innocent_utils-1.0.0.dist-info/     METADATA, WHEEL, RECORD
```

```bash
$ pip install innocent_utils-1.0.0-py3-none-any.whl
$ ls /tmp/pth-ran-*
ls: cannot access '/tmp/pth-ran-*': No such file or directory
$ python -c "pass"
$ ls /tmp/pth-ran-*
/tmp/pth-ran-530
$ python -I -c "pass"        # isolated mode still processes site-packages
$ ls /tmp/pth-ran-*
/tmp/pth-ran-530  /tmp/pth-ran-535
$ python -S -c "pass"        # only -S, which disables site, skips it
```

The install printed nothing and ran nothing. The first unrelated `python` command ran the payload. Isolated mode (`-I`) did not stop it.

Python 3.15 deprecates these `import` lines, and the [`site` documentation](https://docs.python.org/3/library/site.html) says they will be silently ignored in Python 3.18 and 3.19. The replacement is a `name.start` entry point, and the same documentation states that executable startup entries "are always run at Python startup … regardless of whether a particular module is actually going to be used." The change makes startup code explicit and easier to audit. It does not remove startup code.

Source distributions are worse. The [crytic-compilers typosquat](https://socket.dev/blog/trojan-embedded-in-crytic-compilers-python-package) hid its trojan launcher in `setup.py`, and pip runs that file earlier than most developers expect. pip builds metadata from an sdist by running its build backend, so downloading is not inspecting. Our test sdist's `setup.py` wrote a marker file under both of these commands, which never install anything:

```bash
$ pip download --no-deps innocent_sdist-1.0.0.tar.gz
$ pip install --dry-run --no-deps innocent_sdist-1.0.0.tar.gz
# each command left two marker files from setup.py, one per build-backend subprocess
```

## How can malicious code do damage?

Malicious code does damage with the access its host already has: the filesystem, environment variables, the network, and the credentials stored in all three. Developer laptops and CI runners hold registry publish tokens, cloud keys, SSH keys, and signing material, which makes them better targets than most production servers. The payloads we analyze fall into six groups.

| Damage | Mechanism | Documented case |
| --- | --- | --- |
| Credential theft | Reads environment variables, config files, and browser stores, then exfiltrates them | telnyx 4.87.1 encrypted its loot with AES-256-CBC and RSA-4096 before upload |
| Remote access | Installs a backdoor or remote access trojan that takes commands | plain-crypto-js dropped a macOS RAT that beaconed every 60 seconds |
| Self-propagation | Uses stolen publish tokens to inject itself into the victim's other packages | Shai-Hulud, from September 2025 |
| Destruction | Deletes files or kills processes, often after a delay | shanhai666 NuGet packages: 9,488 downloads, a 20% chance of killing the host process on each database query after the trigger date |
| Resource theft | Runs a cryptominer | @rspack/core 1.1.7 ran XMRig capped at 75% of CPU threads |
| Persistence and anti-forensics | Survives reboots or erases its own traces | plain-crypto-js deleted its dropper and swapped in a clean package.json |

Most of the campaigns on this page steal credentials, and an [infostealer](https://socket.dev/glossary/infostealer) payload is the usual shape. The [shanhai666 NuGet packages](https://socket.dev/blog/9-malicious-nuget-packages-deliver-time-delayed-destructive-payloads) show the patient version of destruction: the SQL Server variant arms on August 8, 2027, long after anyone would connect a crash to the install that delivered it. The [Rspack compromise](https://socket.dev/blog/rspack-supply-chain-attack) shows the noisy version of resource theft, shipped in a package with roughly 370,000 weekly downloads.

Anti-forensics deserves its own sentence. After the `plain-crypto-js` `postinstall` hook ran, the installed directory looked like a normal copy of crypto-js, with no trace of the hook. Inspecting `node_modules` after the fact would have found nothing.

Malicious code also adapts to whatever inspects it. In June 2026 we analyzed [an npm package that used prompt injection and 3.5 million tokens of padding](https://socket.dev/blog/npm-package-uses-prompt-injection-and-token-flooding-to-disrupt-ai-malware-scanners) to derail AI-based scanners. Our conclusion from that case: "The scanner is now part of the threat model."

## Malicious package vs. vulnerable package

A malicious package and a vulnerable package both show up as alerts on a dependency, but they need opposite handling.

|  | Vulnerable package | Malicious package |
| --- | --- | --- |
| Origin | A mistake by the author | Code written to cause harm |
| Identifier | CVE, mirrored in GHSA and OSV | Usually no CVE. An OSV MAL- record, a GitHub malware advisory, CWE-506 |
| When it hurts | When an attacker reaches the flaw | On install, build, startup, or import |
| Does reachability matter? | Yes. Most flagged CVEs are never reached | No. The payload calls itself |
| Fix | Upgrade to a patched version | Remove it, treat the host as compromised, rotate secrets, rebuild |
| Patched version | Usually exists | Does not exist. "Fixed" means the last clean release |
| Evidence of exposure | The current dependency tree | Point-in-time logs. Today's tree may look clean |

**Verdict:** route malicious-package alerts to incident response, not the vulnerability backlog. Severity scores, reachability, and "no fix available" logic should never decide malware triage.

The CVE program has long treated malware as out of scope. When the CVE Board discussed malicious npm packages in 2018, CERT/CC's Art Manion summarized the case in [one line](https://www.cve.org/Resources/Media/Archives/OldWebsite/data/board/archives/2018-08/msg00010.html): "Not a technical vulnerability, I accidentally installed malware." Exceptions exist, and they arrive late. The [TanStack compromise](https://socket.dev/blog/tanstack-npm-packages-compromised-mini-shai-hulud-supply-chain-attack) pushed 84 malicious versions across 42 packages on May 11, 2026, and [CVE-2026-45321](https://github.com/advisories/GHSA-g7cv-rxg3-hmpx) reached NVD on May 12. A scanner that only matches CVEs sees malware after the fact, if it sees it at all.

Malicious packages get their own records instead. This is the [OSV](https://socket.dev/glossary/osv) entry for the axios dropper, trimmed:

```json
{
  "id": "MAL-2026-2306",
  "published": "2026-03-31T02:07:58Z",
  "aliases": ["GHSA-2x9r-6wxq-hrr7"],
  "summary": "Malicious code in plain-crypto-js (npm)",
  "affected": [{
    "package": { "ecosystem": "npm", "name": "plain-crypto-js" },
    "ranges": [{ "type": "SEMVER", "events": [{ "introduced": "0" }] }],
    "database_specific": { "cwes": [{ "cweId": "CWE-506", "name": "Embedded Malicious Code" }] }
  }],
  "database_specific": {
    "iocs": { "domains": ["sfrclak.com"], "ips": ["142.11.206.73"] },
    "malicious-packages-origins": [
      { "source": "amazon-inspector", "versions": ["4.2.1"] },
      { "source": "ghsa-malware", "id": "GHSA-2x9r-6wxq-hrr7" },
      { "source": "google-open-source-security", "versions": ["4.2.0", "4.2.1"] }
    ]
  }
}
```

Three details matter. The summary uses the fixed form "Malicious code in \<package\> (\<ecosystem\>)" that the dataset uses for its records. The range starts at `"introduced": "0"` with no fix event, because the whole package is hostile and no version is safe. The record carries indicators of compromise, the domain and IP the dropper contacted, which is what responders need and what a CVE record has no field for. The merged [GitHub advisory](https://github.com/advisories/GHSA-2x9r-6wxq-hrr7) states the operational meaning directly: "Any computer that has this package installed or running should be considered fully compromised."

## Detection speed vs. the public record

Public malware databases record attacks hours after they start, and the axios compromise shows the gap. We rebuilt the timeline from npm registry timestamps, the detection time in our original report, and the OSV record's metadata.

> Axios compromise timeline, March 30 to 31, 2026, UTC. Socket flagged plain-crypto-js 6 minutes after publish; the first public OSV record appeared 2 hours 9 minutes after publish; npm replaced the package 3 hours 26 minutes after publish.

| Time (UTC) | Event | Elapsed from malicious publish |
| --- | --- | --- |
| Mar 30, 05:57:32 | plain-crypto-js 4.2.0 published, a clean copy of crypto-js | 18 hours before |
| Mar 30, 23:59:12 | plain-crypto-js 4.2.1 published with a postinstall dropper | 0 |
| Mar 31, 00:05:41 | Socket flags plain-crypto-js as malware | 6m 29s |
| Mar 31, 00:21:58 | axios 1.14.1 published with the new dependency | 22m 46s |
| Mar 31, 01:00:57 | axios 0.30.4 published, poisoning the 0.x branch | 1h 01m 45s |
| Mar 31, 02:07:58 | OSV record MAL-2026-2306 published | 2h 08m 46s |
| Mar 31, 03:25:11 | npm replaces the package with 0.0.1-security | 3h 25m 59s |
| Mar 31, 04:26:59 | GitHub's malware advisory record last modified | 4h 27m 47s |

A blocklist fed only by public databases would have let `axios` 1.14.1 install for 1 hour and 46 minutes before the first OSV record existed. The clean 4.2.0 release published 18 hours earlier also mattered: it meant the package was no longer brand new when the payload arrived. Databases are the record of an attack. They cannot be the gate.

Databases also make mistakes. In May 2026, OSV [withdrew 157 malware reports](https://socket.dev/blog/osv-withdraws-157-malware-reports) after automated detections flagged legitimate releases, including FastAPI 0.136.3, and those records had already flowed into CI gates and policy engines.

Socket splits its verdicts along the same line. The [Known Malware](https://socket.dev/alerts/malware) alert covers packages a researcher confirmed, or that contain files already confirmed as malware, and Socket's default policy blocks it. [AI-Detected Potential Malware](https://socket.dev/alerts/gptMalware) comes from static analysis plus an LLM review of the package's code, and it only warns by default, per the [default security policy](https://docs.socket.dev/docs/security-policy-default-enabled-alerts). When a researcher confirms the finding, it becomes Known Malware. The split trades speed against the false-positive failure OSV hit in May. A detection that is minutes old will occasionally be wrong, and a default block on it would break builds.

## Counting malicious packages

The OpenSSF dataset counted 239,136 malicious-package reports across 11 ecosystems as of October 15, 2026, but the raw count measures reporting pipelines and spam floods more than risk.

| Ecosystem | OSV malicious-package reports |
| --- | --- |
| npm | 222,181 |
| PyPI | 11,803 |
| RubyGems | 4,304 |
| NuGet | 782 |
| crates.io | 21 |
| Go | 20 |
| Open VSX and VS Code | 21 |
| Maven, Packagist, Git | 4 |

Two monthly buckets dominate the npm number. November 2025 alone holds 142,163 npm reports, and August 2025 holds 35,099. Together they hold 80% of all npm reports in the dataset. The November spike matches [a TEA token-farming spam wave](https://socket.dev/blog/tea-protocol-spam-floods-npm-but-its-not-a-worm). OpenSSF assigned MAL IDs to the packages, and up to 150,000 of them were taken down from npm. Those packages were garbage published to farm crypto rewards, and our analysis found they did not contain malicious code. They counted the same as a credential stealer.

We think the useful number is different: how long a malicious version of a package you already depend on stays installable before something stops it. For axios that window was minutes for an install-time check and hours for a database. That number, not the size of a dataset, decides whether a build gets hit.

## Controls and what each one misses

No single control stops malicious packages, and each common one misses a specific case. [Malware detection](https://socket.dev/glossary/malware-detection) covers the analysis techniques in depth. The table below shows where each control sits and what it misses. Release-age delays are newly default: [pnpm 11](https://socket.dev/blog/pnpm-11-adds-new-supply-chain-protection-defaults) set a 24-hour minimum release age in May 2026.

| Control | Stops | Misses |
| --- | --- | --- |
| Install-script blocking (npm 12, pnpm 10) | Execution point 1, plus point 2 in npm 12 | Build scripts, startup hooks, import-time and deferred payloads. Native modules need explicit approval |
| Minimum release age (pnpm 11 defaults to 24 hours) | Short-lived malicious versions removed within the window | Anything still undetected when the window ends. Configuration can override it |
| Lockfile with npm ci | Silent upgrades of existing dependencies | New dependencies, update PRs, npx and global installs |
| Provenance and trusted publishing | Publishes from outside the expected pipeline | Compromised maintainers or CI publishing through the legitimate pipeline with valid provenance |
| CVE scanning | Known vulnerabilities | Nearly all malware |
| Registry takedowns and OSV feeds | Repeat installs after detection | The hours before detection |
| Pre-install analysis of package contents | New malicious versions before they execute | Wrong verdicts in both directions, and second stages fetched later from a server |

**Verdict:** combine a release-age delay with analysis of what each new version contains, enforced at install time. Treat install-script blocking as attack-surface reduction and lockfiles as reproducibility, not as detection. [Lockfiles make existing installs deterministic](https://socket.dev/blog/hidden-blast-radius-of-the-axios-compromise), but they do not make new installs safe.

### If you already installed one

If you installed a malicious package, establish exposure by time, not by today's state. Today's lockfile can be clean while a CI job ran `npx` against the poisoned version, and the dropper may have rewritten its own files. Treat every machine that installed or imported it as compromised. Rotate every secret it could read, from a different machine, starting with registry publish tokens, because those are what a worm uses next. Rebuild from a clean host, since removing the package does not remove a second stage.

## The case no default catches

The hardest case remains the one the newest controls were not built for. An attacker with a real maintainer's credentials publishes through the real pipeline, with valid provenance, and ships code that runs at import time instead of install time. The new version satisfies every caret range in the ecosystem the moment it is published, and nothing in its metadata distinguishes it from the previous release. Only what the code does tells it apart, which is why that is what we read.
