diff --git a/src/pentesting-web/dependency-confusion.md b/src/pentesting-web/dependency-confusion.md index 34b5903fcb6..df4675ce6ca 100644 --- a/src/pentesting-web/dependency-confusion.md +++ b/src/pentesting-web/dependency-confusion.md @@ -24,6 +24,44 @@ Key idea: If the resolver can see multiple registries for the same package name If your project references a library that isn’t available in the private registry, and your tooling falls back to a public registry, an attacker can seed a malicious package with that name in the public registry. Your runners/CI/dev machines will fetch and execute it. +### npx binary/package name confusion + +`npx ` can become a **remote package install + execution primitive** even when there is **no private-vs-public registry precedence issue**. If `npx` cannot resolve `` as a local or global executable, npm may reuse that same string as a **package spec**, install it into the npx cache (`~/.npm/_npx/`), prepend the cached `node_modules/.bin` to `PATH`, and execute it. + +Why this is different from classic dependency confusion: +- The confusion is between a **binary name** and a **package name**, not between two registries serving the same dependency. +- The trigger is often **running the command from the wrong directory**, before dependencies are installed, inside CI, or in automation/AI-agent workflows. +- Scoped packages make this easier because `@company/tool` commonly exposes an unscoped binary such as `tool-cli`, and that binary name may be unclaimed on the public npm registry. + +Relevant npm resolution flow (`npm/cli` v11.15.0): +1. Search upward for `node_modules/.bin/` from the current working directory. +2. Check the global bin directory. +3. If unresolved, push `args[0]` into `packages`, so the command name is later treated as a package candidate. +4. Check local/global dependency trees and the npx cache under `~/.npm/_npx/`. +5. If still missing, fetch from the public registry, install into the cache, and execute from cached `node_modules/.bin`. + +Minimal vulnerable pattern: + +```json +{ + "name": "@company/internal-tooling", + "bin": { + "tool-cli": "./bin/run.js" + }, + "scripts": { + "build": "npx tool-cli" + } +} +``` + +If `tool-cli` is not available from the intended local dependency context and the public package name `tool-cli` is unclaimed, an attacker can register it and expose a malicious `bin` entry to obtain code execution on developer laptops, CI runners, or automated agents. + +Practical notes: +- The **current working directory matters** because npm walks upward looking for `node_modules/.bin/`. +- In **non-TTY/CI** contexts npm may only warn that the package will be installed, then continue. +- `npx --package=@company/internal-tooling tool-cli` is safer than relying on implicit name inference because it binds execution to the intended package. +- Hunting targets is straightforward: grep `package.json`, READMEs, CI workflows, scripts, and agent/tool definitions for `npx ` and then check whether `` exists on npm. + ### Unspecified Version / “Best-version” selection across indexes Developers frequently leave versions unpinned or allow wide ranges. When a resolver is configured with both internal and public indexes, it may select the newest version regardless of source. For internal names like `requests-company`, if the internal index has `1.0.1` but an attacker publishes `1.0.2` to the public registry and your resolver considers both, the public package may win. @@ -121,7 +159,7 @@ High-level strategies that work across ecosystems: - Pin and lock: - Use lockfiles that record the resolved registry URLs (npm/yarn/pnpm) or use hash/attestation pinning (pip `--require-hashes`, Gradle dependency verification). - Block public fallback for internal names at the registry/network layer. -- Reserve your internal names in public registries when feasible to prevent future squat. +- Reserve your internal names in public registries when feasible to prevent future squat. For npm, also reserve the **binary names** exposed by internal/scoped packages if they differ from the package name. ## Ecosystem Notes and Secure Config Snippets @@ -386,6 +424,11 @@ These controls do **not** replace lockfiles or trusted publishing, but they redu - [https://docs.npmjs.com/cli/v11/using-npm/changelog/](https://docs.npmjs.com/cli/v11/using-npm/changelog/) - [https://pnpm.io/settings](https://pnpm.io/settings) - [https://bun.sh/docs/runtime/bunfig](https://bun.sh/docs/runtime/bunfig) +- [https://www.landh.tech/blog/20260521-npx-used-confusion-and-its-super-effective/](https://www.landh.tech/blog/20260521-npx-used-confusion-and-its-super-effective/) +- [https://docs.npmjs.com/cli/v11/commands/npx/](https://docs.npmjs.com/cli/v11/commands/npx/) +- [https://github.com/npm/cli/blob/v11.15.0/workspaces/libnpmexec/lib/index.js](https://github.com/npm/cli/blob/v11.15.0/workspaces/libnpmexec/lib/index.js) +- [https://github.com/npm/cli/blob/v11.15.0/workspaces/libnpmexec/lib/file-exists.js](https://github.com/npm/cli/blob/v11.15.0/workspaces/libnpmexec/lib/file-exists.js) +- [https://www.aikido.dev/blog/npx-confusion-unclaimed-package-names](https://www.aikido.dev/blog/npx-confusion-unclaimed-package-names) {{#include ../banners/hacktricks-training.md}}