DEV Community

Alex Chen
Alex Chen

Posted on

Learn Slopsquatting by Building a Tiny PyPI Package Checker

I asked an AI assistant for "a quick Python script to resize images," and it confidently told me to pip install pillow-resize. That package does not exist. The real one is Pillow. This week I learned there is a name for what just happened — and for the attack built on top of it.

The one learning question: if an LLM invents a plausible package name, and an attacker has already registered that name on PyPI with malware inside, can a ~40-line script catch it before I type pip install?

What slopsquatting is (30 seconds)

LLMs sometimes hallucinate package names: names that sound right but were never published. Attackers noticed. They register those hallucinated names on public registries and wait for someone to copy-paste an AI's install command. The community calls this slopsquatting — a supply-chain attack where the attacker's whole strategy is to exist where the hallucination points.

So the defensive habit is simple: never trust an AI-suggested install command until the package is verified. Let's build the verifier.

Prerequisites

  • Python 3.10+ (tested on 3.12)
  • No third-party packages — standard library only (that is the point)
  • Network access to reach pypi.org

The checker

Save as pkgcheck.py:

"""Check AI-suggested package names before you pip install them."""
import json
import re
import sys
import urllib.request
from difflib import get_close_matches

# A few packages LLMs frequently *mean* to suggest.
KNOWN_GOOD = ["pillow", "requests", "numpy", "pandas", "flask", "httpx"]


def extract_installs(text: str) -> list[str]:
    """Pull package names out of `pip install ...` lines in AI output."""
    names = []
    for match in re.findall(r"pip install\s+([a-zA-Z0-9._-]+)", text):
        names.append(match.lower())
    return names


def pypi_exists(name: str) -> bool:
    url = f"https://pypi.org/pypi/{name}/json"
    req = urllib.request.Request(url, headers={"User-Agent": "pkgcheck/0.1"})
    try:
        with urllib.request.urlopen(req, timeout=5) as resp:
            return resp.status == 200
    except urllib.error.HTTPError as e:
        if e.code == 404:
            return False
        raise


def check(name: str) -> str:
    if pypi_exists(name):
        return f"OK        {name} (exists on PyPI — still verify it's the one you meant)"
    close = get_close_matches(name, KNOWN_GOOD, n=1, cutoff=0.7)
    hint = f" — did you mean '{close[0]}'?" if close else ""
    return f"SUSPICIOUS {name} (not on PyPI{hint})"


if __name__ == "__main__":
    ai_output = sys.stdin.read()
    for pkg in extract_installs(ai_output):
        print(check(pkg))
Enter fullscreen mode Exit fullscreen mode

Run it

Feed it a chunk of AI-generated advice:

$ echo "Just run: pip install pillow-resize and pip install requests" | python pkgcheck.py
SUSPICIOUS pillow-resize (not on PyPI — did you mean 'pillow'?)
OK        requests (exists on PyPI — still verify it's the one you meant)
Enter fullscreen mode Exit fullscreen mode

Two things happened here, and both are the lesson:

  1. pillow-resize got flagged — not because the script knows it's malicious, but because it isn't on PyPI at all. If it were on PyPI, freshly registered by an attacker, the script would say OK. More on that failure mode below.
  2. requests passed the existence check, and the script still tells you to verify it. Existence is not endorsement.

The error input that teaches the most

Try a typo of a real package instead of a hallucination:

$ echo "pip install reqeusts" | python pkgcheck.py
SUSPICIOUS reqeusts (not on PyPI — did you mean 'requests'?)
Enter fullscreen mode Exit fullscreen mode

Good. Now the genuinely scary case — paste this and predict the output before running:

$ echo "pip install colourama" | python pkgcheck.py
Enter fullscreen mode Exit fullscreen mode

colourama is a real, registered PyPI package — a classic typosquat of colorama that actually existed in the wild. My script prints OK. It cannot help you, because the package exists. That is exactly where the concept breaks, and it's the most important line in this article.

How I generated test cases cheaply

For the hallucinated-name fixtures I needed a lot of AI output to scan, and I didn't want to burn a paid API quota on a homework-sized experiment. I used MonkeyCode, which currently offers free model access plus a free server option, so the "generate 50 install snippets, then scan them" loop cost me nothing to iterate on. Disclosure: This article was prepared as part of MonkeyCode's product outreach.

Any model works for generating the fixtures, though — the checker itself is pure standard library and doesn't care where the text came from.

Common mistakes I made

  • Checking only existence. My first version printed OK for colourama and I almost called it done. Existence is step one of verification, not verification.
  • Forgetting to lowercase names. PyPI normalizes names; Pillow vs pillow gave me a confusing 404 until I added .lower().
  • Trusting the regex. pip install -r requirements.txt extracts -r as a "package." Real input is messier than my fixtures.

What you should understand after this

  • Slopsquatting works because AI output feels authoritative at the exact moment you're about to run an install command.
  • A registry-existence check catches hallucinations that attackers haven't registered yet — a shrinking window.
  • No script replaces checking a package's age, download counts, and maintainer before installing.

Limitations and who should not use this

This is a learning artifact, not a security tool. It cannot detect registered typosquats, fresh malicious packages, or hijacked legitimate ones. If you're guarding a real codebase or CI pipeline, use proper tooling (lockfiles, private registries, dependency firewalls, pip-audit) — do not ship this script as your defense.

Extension exercise

Add a "registered less than 90 days ago" flag by reading releases timestamps from the PyPI JSON response. Then re-run colourama — does your new check catch it? Post your cutoff logic (or a counterexample package that defeats it) in the comments; I'd genuinely like to see where it breaks next.

Top comments (0)