Python Supply Chain Security Made Easy

Maybe you’ve heard that hackers have been trying to take advantage of open source software to inject code into your machine, and worst case scenario, even the consumers of your libraries or your applications machines. In this quick post, I’ll show you how to integrate Python’s “Official” package scanning technology directly into your continuous integration and your project’s unit tests. While pip-audit is maintained in part by Trail of Bits with support from Google, it’s part of the PyPA organization.

Why this matters

Here are 5 recent, high-danger PyPI security issues supply chain attacks where “pip install” can turn into “pip install a backdoor.” Afterwards, we talk about how to scan for and prevent these from making it to your users.

What happened: A malicious version (8.3.41) of the widely-used ultralytics package was published to PyPI, containing code that downloaded the XMRig coinminer. Follow-on versions also carried the malicious downloader, and the writeup attributes the initial compromise to a GitHub Actions script injection, plus later abuse consistent with a stolen PyPI API token. Source: ReversingLabs

Campaign of fake packages stealing cloud access tokens, 14,100+ downloads before removal

What happened: Researchers reported multiple bogus PyPI libraries (including “time-related utilities”) designed to exfiltrate cloud access tokens, with the campaign exceeding 14,100 downloads before takedown. If those tokens are real, this can turn into cloud account takeover. Source: The Hacker News

Typosquatting and name-confusion targeting colorama, with remote control and data theft payloads

What happened: A campaign uploaded lookalike package names to PyPI to catch developers intending to install colorama, with payloads described as enabling persistent remote access/remote control plus harvesting and exfiltration of sensitive data. High danger mainly because colorama is popular and typos happen. Source: Checkmarx

PyPI credential-phishing led to real account compromise and malicious releases of a legit project (num2words)

What happened: PyPI reported an email phishing campaign using a lookalike domain; 4 accounts were successfully phished, attacker-generated API tokens were revoked, and malicious releases of num2words were uploaded then removed. This is the “steal maintainer creds, ship malware via trusted package name” playbook. Source: Python Package Index Blog

SilentSync RAT delivered via malicious PyPI packages (sisaws, secmeasure)

What happened: Zscaler documented malicious packages (including typosquatting) that deliver a Python-based remote access trojan (RAT) with command execution, file exfiltration, screen capture, and browser data theft (credentials, cookies, etc.). Source: Zscaler

Integrating pip-audit

Those are definitely scary situations. I’m sure you’ve heard about typo squatting and how annoying that can be. Caution will save you there. Where caution will not save you is when a legitimate package has its supply chain taken over. A lot of times this could look like a package that you use depends on another package whose maintainer was phished. And now everything that uses that library is carrying that vulnerability forward.

Enter pip-audit.

pip-audit is great because you can just run it on the command line. It will check against PyPA’s official list of vulnerabilities and tell you if anything in your virtual environment or requirements files is known to be malicious.

You could even set up a GitHub Action to do so, and I wouldn’t recommend against that at all. But it’s also valuable to make this check happen on developers’ machines. It’s a simple two-step process to do so:

  1. Add pip-audit to your project’s development dependencies or install it globally with uv tool install pip-audit.
  2. Create a unit test that simply shells out to execute pip-audit and fails the test if an issue is found.

Part one’s easy. Part two takes a little bit more work. That’s okay, because I got it for you. Just download the file here and drop it in your pytest test directory:

test_pypi_security_audit.py

Here’s a small segment to give you a sense of what’s involved.

def test_pip_audit_no_vulnerabilities():
	  # setup ...
    # Run pip-audit with JSON output for easier parsing
    try:
        result = subprocess.run(
            [
                sys.executable,
                '-m',
                'pip_audit',
                '--format=json',
                '--progress-spinner=off',
                '--ignore-vuln',
                'CVE-2025-53000', # example of skipping an irrelevant cve
                '--skip-editable', # don't test your own package in dev
            ],
            cwd=project_root,
            capture_output=True,
            text=True,
            timeout=120,  # 2 minute timeout
        )
    except subprocess.TimeoutExpired:
        pytest.fail('pip-audit command timed out after 120 seconds')
    except FileNotFoundError:
        pytest.fail('pip-audit not installed or not accessible')

That’s it! When anything runs your unit test, whether that’s continuous integration, a git hook, or just a developer testing their code, you’ll also run a pip-audit audit of your project.

Let others find out

Now, pip-audit tests if a malicious package has been installed, In which case, for that poor developer or machine, it may be too late. If it’s CI, who cares? But one other feature you can combine with this that is really nice is uv’s ability to put a delay on upgrading your dependencies.

Many developers, myself included, will typically run some kind of command that will pin your versions. Periodically we also run a command that looks for newer libraries and updates pinned versions so we’re using the latest code. So this way you upgrade in a stair-step manner at the time you’re intending to change versions.

This works great. However, what if the malicious version of a package is released five minutes before before you run this command. You’re getting it installed. But pretty soon, the community is going to find out that something is afoot, report it, and it will be yanked from PyPI. Here bad timing got you hacked.

While it’s not a guaranteed solution, certainly Defense In Depth would tell us maybe wait a few days to install a package. But you don’t want to review packages manually one by one, do you? For example, for Talk Python Training, we have over 200 packages for that website. It would be an immense hassle to verify the dates of each one and manually pick the versions.

No need! We can just add a simple delay to our uv command:

uv pip compile requirements.piptools --upgrade --output-file requirements.txt --exclude-newer "1 week"

In particular, notice –exclude-newer “1 week”. The exact duration isn’t the important thing. It’s about putting a little bit of a delay for issues to be reported into your workflow. You can read about the full feature here. This way, we only incorporate packages that have survived in the public on PyPI for at least one week.

Hope this helps. Stay safe out there.