diff --git a/src/pentesting-web/dependency-confusion.md b/src/pentesting-web/dependency-confusion.md index 34b5903fcb6..6c2ed2e909d 100644 --- a/src/pentesting-web/dependency-confusion.md +++ b/src/pentesting-web/dependency-confusion.md @@ -28,6 +28,54 @@ If your project references a library that isn’t available in the private regis 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. +### `npx` binary/package-name confusion + +A related **execution-time** confusion exists in the npm ecosystem when developers, CI jobs, documentation, wrappers, or AI agents run `npx ` assuming `` will resolve to a known local binary. + +This is **not** the classic private-vs-public registry precedence issue. The problem is that `npx`/`npm exec` may reinterpret the unresolved **binary name** as a **package spec**, install it into the per-user npx cache, and then execute it. + +Typical dangerous pattern: + +```json +{ + "name": "@company/internal-tool", + "bin": { + "build-assets": "./bin/run" + }, + "scripts": { + "build": "npx build-assets" + } +} +``` + +If `npx build-assets` is executed **outside the project/workspace where `node_modules/.bin/build-assets` is visible**, npm walks several locations looking for the binary: + +1. local `node_modules/.bin` (walking up parent directories) +2. global bin directory +3. local dependency tree +4. global dependency tree +5. `~/.npm/_npx/` cache +6. public registry install into the npx cache + +The dangerous transition happens when npm fails to resolve the binary and internally treats `args[0]` as a package candidate (`packages.push(args[0])` in `libnpmexec`). If the name is unclaimed on the public registry, an attacker can register a package with the same name, expose a matching `bin`, and get code execution when the victim runs `npx `. + +Why this is especially common with **scoped packages**: +- Internal packages are often named like `@company/package`. +- Their exported binary cannot contain `/`, so the binary is usually an **unscoped** name such as `build-assets`. +- The package name and executable name are therefore different by design. If the unscoped binary name is not reserved publicly, it becomes squattable. + +Practical red-team checks: +- Search exposed bundles, repos, docs, shell history, CI workflows, container entrypoints, and agent instructions for `npx ` or `npm exec`. +- Enumerate `bin` names from internal/scoped packages and check whether the **binary name itself** exists as a public npm package. +- Re-test from a **different working directory/workspace** or a fresh CI-like environment: the same command can be safe inside the project root but exploitable elsewhere because `localFileExists()` walks parent directories looking for `node_modules/.bin/`. +- Once installed, malicious packages persist under `~/.npm/_npx//`, so later executions may reuse the attacker-controlled cached environment without touching the project dependencies. + +Technical mitigation notes: +- Prefer `npm exec --package= -- ` or `npx --package= ` when you really want a remote package; this disables package-name inference from the first positional argument. +- Prefer `npm run