# Install script attacks: postinstall, setup.py and build.rs

> How npm, PyPI, Cargo and Composer run code at install time, the 2026 attacks that abused each hook, which settings help, and what those settings miss.

Source: https://chain305.com/learn/install-script-attacks/

---

# What is an install script attack?

The package runs its own code before you import it.

An install script attack is malicious code executed while a package is installed or built, through a hook such as an npm postinstall script, a Python setup.py build or a Cargo build.rs, before any application code imports the package. The payload runs on a developer laptop or CI runner with access to tokens, SSH keys and cloud credentials. The sections below cover where each package manager runs install-time code, seven 2026 attacks built on the hooks, and what the settings for turning the hooks off still miss.

By [Muhammad Zeeshan Sultan](https://chain305.com/about/#founder), founder of Chain305 · November 2026

Questions and answers

## What is an install script attack?

An install script attack is malware that runs while a package is installed or built, before any of your code imports the package. Package managers let a package run its own code at install time for legitimate jobs such as compiling a native extension: npm runs `preinstall`, `install` and `postinstall` scripts, pip builds a source distribution through its `setup.py`, Cargo compiles and runs a crate’s `build.rs`, and Composer loads plugins. An attacker who publishes a malicious version, through a hijacked maintainer account or a lookalike name, puts the payload in one of those hooks. The payload then runs on a developer laptop or a CI runner with that user’s access to tokens, SSH keys and cloud credentials. In practice, in the March 2026 [axios compromise](https://chain305.com/teardowns/axios-npm-compromise/), a `postinstall` hook in the injected dependency `plain-crypto-js` installed a remote-access trojan on macOS, Windows and Linux during an ordinary `npm install`.

## Where does install-time code run in each ecosystem?

Install-time code runs in a different place in each ecosystem, and attackers in 2026 used most of them. The npm docs, the Python `site` docs, the Cargo Book and the Composer docs describe each hook:

-   **npm:** `preinstall`, `install` and `postinstall` scripts run on install. A git dependency’s `prepare` script runs before npm packs the package, and a package with a root `binding.gyp` and no install script gets `node-gyp rebuild` by default.
-   **PyPI:** installing a source distribution runs its build backend, usually `setup.py`. A wheel runs nothing at install, but a `.pth` file in `site-packages` runs its `import` lines every time Python starts.
-   **Cargo:** a crate’s `build.rs` is compiled and run just before Cargo builds the crate.
-   **Composer:** only the root package’s own scripts run, but a dependency that is a Composer plugin runs code during the Composer run once you allow the plugin.

A check that looks only for `postinstall` misses most of these hooks.

## What did install script attacks look like in 2026?

Install script attacks in 2026 were spread across six different npm, PyPI and Cargo hooks, not only the obvious `postinstall`. Seven cases from Chain305’s teardowns, built from the incident reports, show the range:

-   **[axios](https://chain305.com/teardowns/axios-npm-compromise/) (npm, March):** a `postinstall` hook in the new dependency `plain-crypto-js` dropped a remote-access trojan.
-   **[pgserve](https://chain305.com/teardowns/pgserve-canistersprawl-npm/) (npm, April):** a `postinstall` stealer republished itself with the victim’s npm tokens.
-   **[@bitwarden/cli 2026.4.0](https://chain305.com/teardowns/bitwarden-cli-npm-compromise/) (npm, April):** the first `preinstall` script in any release downloaded Bun and ran a 9.7 MB payload.
-   **[TanStack](https://chain305.com/teardowns/mini-shai-hulud-tanstack/) (npm, May):** the `prepare` script of a git-hosted optional dependency stole credentials.
-   **[Miasma](https://chain305.com/teardowns/miasma-npm-worm/), Phantom Gyp wave (npm, June):** a 157-byte `binding.gyp` ran a 4.5 MB `index.js` through `node-gyp`, with no install script declared.
-   **[litellm 1.82.8](https://chain305.com/teardowns/litellm-pypi-compromise/) (PyPI, March):** a `.pth` file ran the stealer at every Python start.
-   **[onering 1.4.1](https://chain305.com/teardowns/onering-crates-build-script/) (crates.io, June):** a `build.rs` sent each build’s latest commit and diff to an attacker.

## Which package-manager settings help?

Four package-manager settings turn off or narrow install-time code, and each is one line of configuration:

-   **npm:** `ignore-scripts=true` in `.npmrc` stops npm running the scripts in `package.json` files, as CISA advised after the axios compromise.
-   **pnpm:** a dependency not listed in `allowBuilds` may not run build scripts, and `strictDepBuilds`, on by default, fails the install when one tries.
-   **pip:** `--only-binary :all:` installs wheels only, so no `setup.py` or build backend runs. uv’s `no-build` and Poetry’s `installer.only-binary` do the same.
-   **Composer:** `allow-plugins` defaults to an empty list since Composer 2.2.0, so no plugin loads until you allow the plugin by name.

Cargo runs `build.rs` before it builds the crate, and the Cargo Book says a build script “may perform any number of tasks”, so read each new `build.rs` in a dependency diff. A package which needs its script, for instance a native module, is allowed by name. The guides for [npm](https://chain305.com/learn/block-malicious-npm-packages/), [PyPI](https://chain305.com/learn/block-malicious-pypi-packages/) and [Maven, Go and Cargo](https://chain305.com/learn/maven-go-cargo-package-security/) give the full settings.

## What do these settings miss?

Install-script settings stop install hooks, not code that runs later. A wheel’s `.pth` file passes `--only-binary :all:` and runs at the next Python start, which is how litellm 1.82.8 ran in March 2026. Microsoft’s `durabletask` 1.4.1 to 1.4.3 ran nothing during `pip install`: the dropper fired when the package was imported. The AsyncAPI wave of the Miasma worm ran at module load, and JFrog notes that `--ignore-scripts` does not help there. npm’s `ignore-scripts` also leaves `npm start`, `npm test` and `npm run` running the script they name. The settings work per machine and per project, so one laptop or one CI job without the line is exposed. And the switches are all or nothing per package: a native module that needs its script gets an exception, and from then on every new version of that module runs its script unchecked. None of the settings reads what a script does.

## Where does a package firewall fit?

A package firewall reads the install-time code when the package is first fetched and refuses the package before the hook can run, for every client that pulls through the firewall. Chainsaw ships two seeded rules for install-time code. The first refuses install scripts that fetch remote URLs: Chainsaw follows a `postinstall` command into the script it runs. When we measured Chainsaw against 58 attacks from 2026, the rule refused `pgserve`, the SAP CAP `preinstall` loader and `onering`’s `build.rs` on first download. The second refuses import-time execution of fetched or decoded code, which refused litellm 1.82.8’s `.pth` file and all three `durabletask` versions. A 48-hour release hold, longer when a version first adds an install script, covers what the rules miss. The rules have limits: axios’s `setup.js` decoded its strings without a direct download call, so the age rule stopped axios, not the script rule. [The 2026 attack results.](https://chain305.com/attack-coverage-2026/)

## Sources

Each fact on this page links its source, and the list below is the full set: the npm, pnpm, pip, Python, Cargo and Composer documentation, the CISA alert on axios, a StepSecurity report, and Chain305’s teardowns of nine 2026 attacks.

-   npm Docs, [scripts: lifecycle scripts, `prepare` for git dependencies and the `binding.gyp` default](https://docs.npmjs.com/cli/v11/using-npm/scripts) and [config: `ignore-scripts`](https://docs.npmjs.com/cli/v11/using-npm/config).
-   pnpm, [build settings: `allowBuilds`, `strictDepBuilds`, `ignoreScripts`](https://pnpm.io/settings/build).
-   pip docs, [secure installs](https://pip.pypa.io/en/stable/topics/secure-installs/) and [build system](https://pip.pypa.io/en/stable/reference/build-system/).
-   Python docs, [`site`: `.pth` files](https://docs.python.org/3/library/site.html).
-   The Cargo Book, [build scripts](https://doc.rust-lang.org/cargo/reference/build-scripts.html).
-   Composer, [scripts](https://getcomposer.org/doc/articles/scripts.md) and [config: `allow-plugins`](https://getcomposer.org/doc/06-config.md#allow-plugins).
-   CISA, [Supply Chain Compromise Impacts Axios Node Package Manager](https://www.cisa.gov/news-events/alerts/2026/04/20/supply-chain-compromise-impacts-axios-node-package-manager), 20 April 2026.
-   StepSecurity, [binding.gyp npm supply chain attack spreads like a worm](https://www.stepsecurity.io/blog/binding-gyp-npm-supply-chain-attack-spreads-like-worm), 3 June 2026.
-   Chain305 teardowns: [axios](https://chain305.com/teardowns/axios-npm-compromise/), [pgserve](https://chain305.com/teardowns/pgserve-canistersprawl-npm/), [Bitwarden CLI](https://chain305.com/teardowns/bitwarden-cli-npm-compromise/), [TanStack](https://chain305.com/teardowns/mini-shai-hulud-tanstack/), [SAP CAP](https://chain305.com/teardowns/sap-cap-npm-compromise/), [Miasma](https://chain305.com/teardowns/miasma-npm-worm/), [litellm](https://chain305.com/teardowns/litellm-pypi-compromise/), [durabletask](https://chain305.com/teardowns/durabletask-pypi-compromise/) and [onering](https://chain305.com/teardowns/onering-crates-build-script/).

Install hooks run on first download

## Read the install script before it runs

Start a free org in monitor mode and see which install scripts Chainsaw would refuse in your dependency tree.

[Start in monitor mode](https://chain305.com/chainsaw/signup) [Read the policy rules →](https://chain305.com/product/policy/)
