npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

evil-pkg

v1.0.5

Published

PoC: PATH poisoning via node_modules/.bin - untrusted packages can shadow system commands in Bun's package manager

Readme

evil-pkg - Self-Triggering PATH Poisoning PoC for Bun

This is a proof of concept for a PATH poisoning vulnerability in bun install. Starting with v1.0.4, no victim scripts are needed. A bare bun i evil-pkg in an empty project achieves RCE.

What changed in v1.0.4

Previous versions (<=1.0.3) required the victim project to have its own postinstall script that calls node. That limited the attack to projects with lifecycle scripts.

v1.0.4 brings its own trigger. It declares esbuild as a dependency. Bun maintains a default-trusted-dependencies list of ~367 packages whose lifecycle scripts run automatically without --trust or trustedDependencies. esbuild is on that list, and its postinstall runs node install.js.

The attack chain:

  1. Victim runs bun i evil-pkg (or evil-pkg is a transitive dependency)
  2. Bun resolves evil-pkg + esbuild
  3. Bun creates node_modules/.bin/node../evil-pkg/shim.js (from the bin field)
  4. Bun sees esbuild is default-trusted, so it runs esbuild's postinstall: "node install.js"
  5. PATH has node_modules/.bin prepended → node resolves to the attacker's shim
  6. Attacker code executes with the victim's full privileges

No victim scripts. No trustedDependencies. No --trust flag. Just: bun i evil-pkg.

Why it happens

Bun creates node_modules/.bin/<name> symlinks for every package that declares a bin field. These symlinks are created during extraction, before any scripts run. The trust system never looks at them.

Later, when any lifecycle script executes, Bun walks up from the package directory and prepends every node_modules/.bin it finds to PATH. If an attacker's package declared "bin": {"node": "./payload"}, then node_modules/.bin/node points to attacker code. When a trusted package's postinstall calls node, the shell resolves it from .bin/ first.

Four design gaps chain together:

  1. normalized_bin_name() in src/install/bin.rs strips path separators and rejects . and ... But it passes node, sh, bash, make, gcc, curl, git, and every other system command through without question.

  2. .bin/ symlinks are created for all packages regardless of trust. Blocking a package's own lifecycle scripts does nothing to stop its bin entries from landing in the shared .bin/ directory.

  3. PATH construction in PackageManagerLifecycle.rs prepends node_modules/.bin globally. Every package that runs scripts sees every other package's bin entries.

  4. 367 packages are default-trusted (src/install/default-trusted-dependencies.txt). An attacker can depend on any of them to provide a self-contained trigger. The victim never needs to configure anything.

Proof of concept

mkdir /tmp/test-bunnyhijack && cd /tmp/test-bunnyhijack
echo '{"name":"app","version":"1.0.0"}' > package.json
bun i evil-pkg
cat /tmp/.bun-npm-pwned

That's it. An empty package.json with no scripts, no trustedDependencies, nothing.

Result:

bun add v1.3.14

[!] PATH POISONED - [email protected] just hijacked your 'node' command.

What just happened?
  -> You ran: bun i evil-pkg
  -> evil-pkg declared esbuild as a dependency.
  -> esbuild is on Bun's default-trusted list, so its postinstall runs automatically.
  -> esbuild's postinstall calls `node install.js`.
  -> But evil-pkg's bin field shadows `node` with this shim.
  -> node_modules/.bin/node → evil-pkg/shim.js
  -> So esbuild's postinstall executed ATTACKER code instead of real node.

  No victim scripts. No trustedDependencies. No --trust flag.
  Just: bun i evil-pkg

installed [email protected] with binaries:
 - node

PWNED

Default-trusted packages that can be abused as triggers

Any of these 367 packages can replace esbuild as the trigger. High-value targets that call node in postinstall:

| Package | postinstall calls | |---------|------------------| | esbuild | node install.js | | sharp | node install/check.js | | bcrypt | node-pre-gyp install (calls node) | | sqlite3 | node-pre-gyp install | | node-sass | node scripts/build.js | | puppeteer | node install.mjs | | cypress | node index.js --exec install | | canvas | node-pre-gyp install | | argon2 | node-pre-gyp install |

An attacker can shadow node, sh, make, gcc, python, curl — all at once:

{
  "bin": {
    "node": "./p", "sh": "./p", "make": "./p",
    "gcc": "./p", "curl": "./p"
  },
  "dependencies": { "esbuild": ">=0.17.0" }
}

Attack complexity

Attack complexity is low. The attacker publishes one package. The victim runs bun i <package>. That's it.

The payload runs with the victim's full user privileges. It can read .env, SSH keys, .npmrc tokens, cloud credentials. It can write to shell startup files, Git hooks, desktop autostart entries.

What bun should do

  1. normalized_bin_name() needs to reject bin names that match system commands. At minimum: node, sh, bash, make, gcc, g++, cc, python, python3, curl, wget, git, npm, npx, bun.

  2. Scope .bin/ entries per-package instead of sharing a global directory. Only add a package's own bin entries to PATH when running its scripts.

  3. The default-trusted list needs to account for the fact that trusted packages' scripts run in a PATH that includes untrusted packages' bin entries. Trust is not transitive — a trusted script should not be forced to execute an untrusted binary.

Affected versions

All versions of Bun since the package manager was introduced. Confirmed on v1.3.14 (latest release).

Credits

Discovered during security research on Bun. GitHub: https://github.com/X3r0Day/BunnyHijack