Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion src/pentesting-web/dependency-confusion.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name>` can become a **remote package install + execution primitive** even when there is **no private-vs-public registry precedence issue**. If `npx` cannot resolve `<name>` 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/<name>` 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/<name>`.
- 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 <name>` and then check whether `<name>` 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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}}