How to Check Your Python Environment for the LiteLLM Credential Stealer (and Protect Your AI API… A malicious release, litellm==1.107.0, was published to PyPI and to LiteLLM's official Docker images, executing credential-stealing code at Python startup without the library ever being imported, according to LiteLLM's official incident post. All seven wheels were live on PyPI from 11:39 to 13:23 UTC on October 21, 2026, while Docker Hub tags v1.107.0, latest, main-latest and main-stable pointed to a poisoned image from 11:46 to 14:20 UTC and the same GHCR tags were affected until 13:51 UTC. LiteLLM attributes the compromise to stolen GitHub CLI tokens from three developers, used to push past branch protection and run the release pipeline from a lookalike repository named Berri-AI/litellm, and recommends pinning to 1.106.2 for a library with about 23 million weekly downloads. Follow me on 🔗 Linkedin https://www.linkedin.com/in/prajwal-waykos/ , 🐙 GitHub https://github.com/praj-17 “I never even imported it.” Famous last words of a Python developer. Hey there, fellow tech-enthusiast On October 21, 2026, I saw the words “LiteLLM” and “compromised” in the same sentence and did what any calm, rational engineer would do. I panicked a little. LiteLLM is the kind of library that quietly ends up in a lot of AI side projects, routing calls to OpenAI, Anthropic and friends. So I spent my evening checking every virtualenv I own. Turns out I own a lot of them. One was called test2-final-REAL. I don't remember making it. The good news is that the check is not hard once you know where to look. The not so good news is that the usual first instinct, typing pip show litellm, can actually make things worse. Let me show you why, and then let me give you a small scanner that does the job safely. A malicious release, litellm==1.107.0, was published to PyPI and to the official Docker images. According to the official LiteLLM incident post https://docs.litellm.ai/blog/security-incident-oct-21-2026 , all seven wheels were live on PyPI from 11:39 to 13:23 UTC, roughly two hours. The Docker Hub tags v1.107.0, latest, main-latest and main-stable pointed to a poisoned image from 11:46 to 14:20 UTC. The same tags on GHCR were affected until 13:51 UTC. How did it get there? The attackers used stolen GitHub CLI tokens from three LiteLLM developers. LiteLLM believes the tokens leaked through logs of an internal deployment that was exposed to the internet. With those tokens, the attackers pushed past branch protection, ran the release pipeline from a lookalike repository called Berri-AI/litellm note the extra dash , and then deleted workflow records to hide their tracks. The official source code on GitHub was clean. Only the published packages were poisoned. LiteLLM says every earlier version is safe and recommends pinning to 1.106.2. With about 23 million weekly downloads https://securitylabs.datadoghq.com/articles/litellm-compromised-pypi-docker/ , two hours is plenty of time for a lot of CI pipelines to run pip install litellm without a version pin. Here’s the bit that surprised me most. You did not need to import LiteLLM for the malware to run. Every time Python starts, a built-in module called site scans your site-packages folder. If it finds a file ending in .pth, it reads it line by line. Most lines are just folder paths. But any line that starts with import gets executed as Python code. Right there, before your script runs a single line. After that, Python also imports a file called sitecustomize.py if it finds one. Again, automatically. The bad wheel abused both. Microsoft’s analysis https://www.microsoft.com/en-us/security/blog/2026/10/23/inside-the-litellm-1-107-0-supply-chain-compromise-credential-theft-through-python-startup-hooks/ explains that it shipped three extra files which landed directly in site-packages, outside the litellm folder. Here’s a harmless experiment you can run yourself. It creates a fresh venv and drops in a .pth file that only prints a message. 1. make a throwaway virtual environmentpython3 -m venv demo-venv 2. ask the venv where its site-packages folder livesSITE=$ demo-venv/bin/python -c "import sysconfig; print sysconfig.get paths 'purelib' " 3. drop in a harmless .pth file with an import lineecho 'import sys; print "hello from harmless test.pth, I ran before your code " ' "$SITE/harmless test.pth" 4. run some code that has nothing to do with that filedemo-venv/bin/python -c 'print "my actual code" ' And the output on my machine. hello from harmless test.pth, I ran before your code hello from harmless test.pth, I ran before your code my actual code It even said hello twice. Very polite, very terrifying. Now swap that print for “zip up every API key on this laptop and mail it somewhere” and you get the idea. This is also why pip show litellm is a bad first move. pip is a Python program, so starting it inside an infected environment fires the stealer again. Microsoft even saw one case where a routine pip install of an unrelated package triggered the collector. Microsoft and Datadog list cloud credentials for AWS, Azure and GCP, Kubernetes and Docker configs, SSH and GPG keys, Git and package manager tokens, .env files, shell history, browser data, and the configs of AI coding tools. On a LiteLLM proxy it also tried to read stored provider keys straight out of the proxy database. The haul was encrypted and sent to www.litellm . dev/healthcheck. That domain looks official. It is not. The golden rule is simple. Look at files, don’t run the suspect Python. These shell commands only read folder and file names. every installed litellm version, read from folder namesfind ~ -type d -name "litellm- .dist-info" 2 /dev/null the three files the malicious wheel dropped into site-packagesfind ~ -path " -packages/ " \ -name "cook.pth" -o -name "cook.py" -o -name "sitecustomize.py" \ 2 /dev/null cached copies of the bad wheel pip and uv caches find ~/.cache -name "litellm-1.107.0 " 2 /dev/null marker files left by the malwarels -la /tmp/.pc once 72129118c59f5636c0bb /tmp/remote code loader.lock 2 /dev/null A litellm-1.107.0.dist-info folder or a cook.pth file means trouble. A sitecustomize.py is not automatically evil, since some setups use one on purpose, but open it and read it. Also, a missing marker file proves nothing. Containers and tmp cleanup wipe them. Checking venvs by hand got old fast, so I wrote a tiny scanner. It only reads files. It never imports LiteLLM and never starts your venv’s Python. It looks for the known file names, the SHA-256 hashes published by Microsoft, any 1.107.0 leftovers, the marker files, and every .pth line that runs code. Run it with your system Python and two flags. -I ignores Python environment variables and your user site-packages. -S skips the site module, so no .pth file gets a chance to run. """scanner.py: look for signs of the compromised litellm==1.107.0 release.It only READS files. It never imports litellm and never runs your venv's Python.Run it with a clean interpreter and startup hooks switched off: python3 -I -S scanner.py ~/projects ~/.cache-I ignores PYTHON environment variables and your user site-packages-S skips the "site" module, so no .pth file or sitecustomize.py gets to run"""import hashlibimport osimport sysimport sysconfig File names and SHA-256 hashes published by LiteLLM, Microsoft and DatadogBAD FILE NAMES = {"cook.pth", "cook.py"}BAD HASHES = { "7f191632e7320705d154b85204f1ae546b543acbd7e8861229b76f5042b92bdb": "cook.pth", "84dc8c82c0a4a4a62552ef826b49fcc86d1a5c4a5d0f2c06665723f93d129d7d": "cook.pth variant ", "9dd0a3926f96a37eb86cf1ee2310d9be93139cc4ebef3ef9e2efe9531390be25": "cook.py", "03ac2a8b752605d1d7710979c17ff18b03dd978203ba23ba97fd8ce3d6f897ea": "cook.py Windows ", "34bef04fe88ac9d0aae0475630b1738681d15b94f233ac8aa46eacf5968305fa": "sitecustomize.py", "b6122440d0d9cb1eb1ade9933f49214da8c31288de7710c076c2680dfa528465": "sitecustomize.py Windows ",} Marker files the malware leaves behind missing does NOT mean you are safe MARKER FILES = "/tmp/.pc once 72129118c59f5636c0bb", "/tmp/remote code loader.lock" Common, harmless .pth files that also contain "import" linesUSUALLY FINE = {"distutils-precedence.pth", " virtualenv.pth"}SKIP DIRS = {".git", "node modules", "proc", "sys", "dev"}findings = list of level, message def report level, message : findings.append level, message print f" {level} {message}" def sha256 of path : digest = hashlib.sha256 with open path, "rb" as f: for chunk in iter lambda: f.read 65536 , b"" : digest.update chunk return digest.hexdigest def check pth path : """Python executes any .pth line that starts with 'import'. Show those lines.""" name = os.path.basename path with open path, encoding="utf-8", errors="replace" as f: for number, line in enumerate f, start=1 : if line.startswith "import ", "import\t" : level = "INFO" if name in USUALLY FINE or name.startswith " editable " else "REVIEW" report level, f"{path} line {number} runs code at startup: {line.strip :80 }" def check site packages folder : for name in os.listdir folder : path = os.path.join folder, name if name.startswith "litellm-" and name.endswith ".dist-info" : version = name len "litellm-" :-len ".dist-info" level = "DANGER" if version == "1.107.0" else "INFO" report level, f"litellm {version} installed in {folder}" if not os.path.isfile path : continue if name in BAD FILE NAMES: report "DANGER", f"known malicious file name: {path}" if name == "sitecustomize.py": report "REVIEW", f"sitecustomize.py runs on every start, check it: {path}" if name.endswith ".pth", ".py" : file hash = sha256 of path if file hash in BAD HASHES: report "DANGER", f"hash matches {BAD HASHES file hash }: {path}" if name.endswith ".pth" : check pth path def find site packages roots : found = {sysconfig.get paths "purelib" } this interpreter's own folder for root in roots: for dirpath, dirnames, filenames in os.walk root : dirnames : = d for d in dirnames if d not in SKIP DIRS for d in dirnames + filenames: e.g. a cached litellm-1.107.0-...whl dist-info is checked later if "litellm-1.107.0" in d and not d.endswith ".dist-info" : report "DANGER", f"1.107.0 artifact: {os.path.join dirpath, d }" if os.path.basename dirpath in "site-packages", "dist-packages" : found.add dirpath dirnames : = no need to walk inside every package return sorted p for p in found if os.path.isdir p def main : roots = sys.argv 1: or os.path.expanduser "~" folders = find site packages roots print f"Checking {len folders } site-packages folder s ...\n" for folder in folders: check site packages folder for marker in MARKER FILES: if os.path.exists marker : report "DANGER", f"malware marker file exists: {marker}" dangers = sum 1 for level, in findings if level == "DANGER" reviews = sum 1 for level, in findings if level == "REVIEW" print f"\nDone. {dangers} danger, {reviews} to review." if dangers: print "Treat this machine as compromised. Do not run pip or python in that env." sys.exit 2 if dangers else 1 if reviews else 0 if name == " main ": main I tested it on a venv with my harmless .pth file from earlier. It flagged exactly that file. bash $ python3 -I -S scanner.py /workspace/scantestChecking 2 site-packages folder s ... REVIEW /workspace/scantest/demo-venv/lib/python3.13/site-packages/harmless test.pth line 1 runs code at startup: import sys; print "hello from harmless test.pth, I ran before your code " Done. 0 danger, 1 to review. A REVIEW line is not a verdict. Plenty of normal tools use .pth files, so just open the file and make sure you recognise it. A DANGER line means stop and go to the response section. The exit code is 0 for clean, 1 for review and 2 for danger, so you can drop it into CI too. If you ran LiteLLM in Docker on October 21, first list your images with their digests and compare against the bad index digest sha256:1bbcb1443bc9... from the LiteLLM post. Then you can export the image's files without ever starting the container and scan them. docker images --digests | grep -i litellm create not run a container from YOUR local image ID, so nothing inside it executesdocker create --name litellm-check