npm and pip Supply Chain Security: Detecting Dependency Confusion and Typosquatting Attacks

Security tutorial - IT technology blog
Security tutorial - IT technology blog

Quick Start: Scan Your Project in 5 Minutes

Run this against your current project right now. You need pip-audit for Python and npm audit for Node — both work in most environments without extra setup.

# Python: audit installed packages
pip install pip-audit
pip-audit

# Node.js: audit npm dependencies
npm audit
npm audit --json | jq '.vulnerabilities | keys'

If you see anything flagged as high or critical, stop before deploying. That’s your baseline check — but it only catches known CVEs. Dependency confusion and typosquatting are a different class of attack entirely, and standard audits miss them. That’s the gap this guide fills.

What Actually Happens in These Attacks

Dependency Confusion (Namespace Confusion)

The setup is simple and brutal. Your company hosts an internal package — say acme-utils — on a private npm registry or PyPI mirror. An attacker publishes a package with the same name on the public registry, but bumps the version number higher. When your CI/CD pipeline runs npm install or pip install, the package manager fetches the public one. Higher version wins. That’s it.

Alex Birsan proved this in 2021 and collected bug bounties from Microsoft, Apple, Shopify, and 32 other companies in a single research disclosure. The payloads executed automatically — no user interaction, no special permissions. Just npm install.

Typosquatting

Someone registers requets instead of requests, or cros-env instead of cross-env. Then they wait. Developers mistype. CI scripts get copy-pasted with errors. Deadline pressure makes people sloppy — and one wrong character in a dependency name is all it takes.

colourama (a typo of colorama) was downloaded over 500 times before PyPI pulled it. The malicious payload runs at install time via postinstall scripts in npm or setup.py in pip — before you’ve even imported the package in your code.

Deep Dive: Detection Techniques

Detecting Typosquatting with difflib and Custom Scripts

Cross-check your requirements.txt against a known-good package list using fuzzy matching. Anything that scores above 0.85 similarity to a real package name — but isn’t on your whitelist — gets flagged.

import difflib

# Known legitimate packages (keep this updated)
KNOWN_PACKAGES = [
    "requests", "flask", "django", "numpy", "pandas",
    "boto3", "fastapi", "pydantic", "sqlalchemy", "celery"
]

def check_typosquatting(package_name, threshold=0.85):
    matches = difflib.get_close_matches(
        package_name,
        KNOWN_PACKAGES,
        n=3,
        cutoff=threshold
    )
    if matches and package_name not in KNOWN_PACKAGES:
        print(f"[WARN] '{package_name}' looks similar to: {matches}")
        return True
    return False

# Read from requirements.txt
with open("requirements.txt") as f:
    for line in f:
        pkg = line.strip().split("==")[0].split(">")[0].split("<")[0]
        if pkg:
            check_typosquatting(pkg)

Not foolproof — but it’s a cheap first pass that catches the obvious cases. Drop it into any CI pipeline and it pays for itself the first time it fires.

Using pip-audit and Safety for Known Malicious Packages

# pip-audit checks against PyPI Advisory Database
pip-audit -r requirements.txt --fix

# Safety checks against a curated vulnerability database
pip install safety
safety check -r requirements.txt --full-report

npm: Detecting Suspicious postinstall Scripts

Nearly every malicious npm package uses lifecycle scripts to execute code at install time. Check what a package declares before it runs:

# Check lifecycle scripts before installing
npm pack some-package --dry-run
npx can-i-ignore-scripts some-package

# Install with scripts disabled (breaks some legit packages, but safe for auditing)
npm install --ignore-scripts

# Scan installed packages for postinstall hooks
cat node_modules/*/package.json | jq -r 'select(.scripts.postinstall) | "\(.name): \(.scripts.postinstall)"'

Checking Package Metadata for Red Flags

# npm: check publish date, maintainer count, download stats
npx npm-package-stats some-package

# PyPI: pull metadata via API
curl -s https://pypi.org/pypi/requests/json | jq '{
  author: .info.author,
  maintainers: .info.maintainer,
  home_page: .info.home_page,
  upload_time: .urls[0].upload_time
}'

Published yesterday. No GitHub link. No maintainer info. Name identical to your internal tooling. That’s the exact fingerprint of a dependency confusion payload — four red flags at once.

Advanced Usage: Locking Down Your Supply Chain

Scoped Packages and Registry Configuration (npm)

Scope internal packages under your org name, then tell npm to resolve that scope exclusively from your private registry:

# .npmrc in project root
@mycompany:registry=https://npm.mycompany.internal
always-auth=true

With this in place, npm never touches the public registry for @mycompany/* packages. Dependency confusion is neutralized at the config level — no code changes needed.

Index Priority in pip

Use --index-url to set your private registry as the primary source, with --extra-index-url as fallback for public packages:

# pip.conf — private registry as primary
[global]
index-url = https://pypi.mycompany.internal/simple/
extra-index-url = https://pypi.org/simple/

The safer pattern skips extra-index-url for internal packages entirely. Vendor them directly, or use a registry proxy like Nexus or Artifactory that mirrors PyPI and lets you enforce an allow-list. That removes the ambiguity pip has when the same package name exists in both registries.

Lockfiles Are Non-Negotiable

Commit your lockfiles. Always. No exceptions.

# Node.js — commit package-lock.json or yarn.lock
git add package-lock.json

# Python — generate and commit a pinned requirements file
pip freeze > requirements.lock
git add requirements.lock

# CI installs from lockfile, not requirements.txt
pip install -r requirements.lock
npm ci  # not npm install

npm ci is the critical difference here. It installs exactly what’s in package-lock.json and hard-fails on any mismatch, rather than silently resolving to a newer version.

Verify Package Integrity with Hashes

# pip: require hash verification
pip install --require-hashes -r requirements.txt

# Generate hashed requirements with pip-compile (from pip-tools)
pip install pip-tools
pip-compile --generate-hashes requirements.in

A hash-pinned requirements.txt looks like this:

requests==2.31.0 \
    --hash=sha256:58cd2187423d77b8d5e82b788 \
    --hash=sha256:942c5a758f98d790eaed1a29c

Downloaded package doesn’t match a listed hash? pip refuses to install. That covers both tampered packages and MITM attacks on the registry itself.

Practical Tips From Real Deployments

Automated Scanning in CI/CD

Wire these up as pipeline steps in GitHub Actions or GitLab CI — they run in under 30 seconds and catch issues before they reach production:

# .github/workflows/security.yml
jobs:
  dependency-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Python audit
        run: |
          pip install pip-audit
          pip-audit -r requirements.txt
      
      - name: npm audit
        run: npm audit --audit-level=high
      
      - name: Check for suspicious install scripts
        run: |
          cat node_modules/*/package.json 2>/dev/null | \
          python3 -c "
import sys, json
for line in sys.stdin:
    try:
        pkg = json.loads(line)
        scripts = pkg.get('scripts', {})
        if any(k in scripts for k in ['preinstall','install','postinstall']):
            print(f\"{pkg.get('name')}: {scripts}\")
    except: pass
"

Namespace Reservation

If your internal packages aren’t scoped, register the names on the public registries yourself. Publish empty placeholder packages with a clear README warning. It’s tedious for five minutes and prevents squatting permanently.

# Register a placeholder on PyPI
# Create a minimal setup.py, add a big warning in the README
# that this is an internal package and the public version is a placeholder
python setup.py sdist upload

Dependency Review on Pull Requests

GitHub’s Dependency Review Action blocks PRs that introduce vulnerable or newly-added high-risk dependencies — before the code ever gets merged:

- uses: actions/dependency-review-action@v4
  with:
    fail-on-severity: high
    deny-licenses: GPL-2.0, AGPL-3.0

Keep Registry Credentials Strong

Private registries need authentication, and those credentials are a target. Rotate them regularly and generate them properly — 32+ characters, full character sets. I use toolcraft.app/en/tools/security/password-generator for registry tokens specifically: it runs entirely in the browser, so nothing touches a network request.

Monitor for New Package Registrations Matching Your Internal Names

Services like socket.dev watch npm in real time and alert when a new package appears matching patterns you define. Set up alerts for your internal package names — you want to know within minutes, not days.

# socket.dev CLI integration
npm install -g @socketsecurity/cli
socket scan .

Lock your files. Scope your internal packages. Disable scripts where possible. Scan in CI. Four habits that eliminate most of the attack surface — and none of them require a dedicated security team to pull off.

Share: