Your pip install Is a Remote Code Execution: Practical Defenses Against Compromised PyPI Packages
本文为原文前 6,000 字符的节选翻译,完整内容请查看原文。
Your pip install Is a Remote Code Execution: Practical Defenses Against Compromised PyPI Packages
This week, Hacker News is buzzing with news of a LiteLLM version on PyPI being injected with malicious code: installing it steals credentials and leaves a backdoor. This is not the first time, nor will it be the last. Many Vietnamese teams are still in the habit of writing pip install -U litellm in Dockerfiles or leaving requirements.txt without pinned versions. Doing so gives anyone who compromises a maintainer’s account the right to run code on your laptop, CI runner, and production server. This article does not discuss theory. I am sharing what my team is applying to reduce risk, along with commands and scripts that can be used immediately.
Tuần này HN lại nóng với tin một bản LiteLLM trên PyPI bị chèn mã độc: cài vào là nó đánh cắp credential và để lại backdoor. Đây không phải lần đầu, cũng chưa phải lần cuối. Nhiều team Việt Nam mình vẫn quen viết pip install -U litellm trong Dockerfile hoặc để requirements.txt không pin version. Làm vậy là đang cho bất kỳ ai chiếm được tài khoản của maintainer quyền chạy code trên laptop, CI runner và production server của mình. Bài này không bàn lý thuyết. Mình chia sẻ những gì team mình đang áp dụng để giảm rủi ro, kèm lệnh và script dùng được ngay.
Why pip install is more dangerous than you think
Many people think malicious packages only cause harm when you import them. In reality, there are at least three ways for code to run much earlier:
setup.pyof a source distribution (sdist): runs immediately during build, before you have a chance to read the code..pthfiles insite-packages: any line in a.pthfile starting withimportwill be executed by Python every time the interpreter starts, even if you just runpython -c "print(1)". This is a persistence technique that recent malicious packages use very often.- Transitive dependency: you don’t install the malicious package, but a library you trust pulls in a new version of it.
Once running, the first thing the malware does is usually scan environment variables (
OPENAI_API_KEY,AWS_SECRET_ACCESS_KEY,GITHUB_TOKEN), read~/.aws/credentials,~/.ssh/,~/.kube/config, and then send them out via HTTPS. CI runners are the juiciest targets because they have deploy secrets ready.
Vì sao pip install nguy hiểm hơn bạn nghĩ
Nhiều người nghĩ package độc chỉ gây hại khi mình import nó. Thực tế có ít nhất ba cách để code chạy sớm hơn thế nhiều:
setup.pycủa source distribution (sdist): chạy ngay lúc build, trước khi bạn kịp đọc code.- File
.pthtrongsite-packages: dòng nào trong.pthbắt đầu bằngimportsẽ được Python thực thi mỗi khi interpreter khởi động, kể cả khi bạn chỉ chạypython -c "print(1)". Đây là kỹ thuật persistence mà các package độc gần đây rất hay dùng. - Transitive dependency: bạn không cài package độc, nhưng một thư viện bạn tin dùng lại kéo version mới của nó về.
Khi đã chạy được, việc đầu tiên mã độc làm thường là quét biến môi trường (
OPENAI_API_KEY,AWS_SECRET_ACCESS_KEY,GITHUB_TOKEN), đọc~/.aws/credentials,~/.ssh/,~/.kube/config, rồi gửi ra ngoài qua HTTPS. CI runner là mục tiêu béo bở nhất vì ở đó có sẵn secret deploy.
Layer 1: Pin versions with hashes, and add a “cooldown”
Pinning litellm==1.x.y is not enough. If an attacker uploads a different wheel file (or you are pointed to a malicious mirror), the version still matches. You must pin the hash of every artifact. With uv (I use version 0.9.x), this is very fast:
# requirements.in only lists direct dependencies
echo "litellm>=1.50" > requirements.in
echo "fastapi" >> requirements.in
# Cooldown: only accept packages published before 7 days ago
CUTOFF=$(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ)
uv pip compile requirements.in \
--generate-hashes \
--exclude-newer "$CUTOFF" \
-o requirements.txt
# In CI / Dockerfile: enforce hashes, do not resolve anything else
pip install --require-hashes --no-deps -r requirements.txt
# Block sdist so no setup.py can run
pip install --only-binary=:all: --require-hashes -r requirements.txt
Lớp 1: Pin version kèm hash, và thêm “cooldown”
Pin litellm==1.x.y thôi thì chưa đủ. Nếu attacker upload lại một file wheel khác (hoặc bạn bị trỏ sang mirror độc), version vẫn khớp. Phải pin luôn hash của từng artifact. Với uv (mình dùng bản 0.9.x), việc này rất nhanh:
# requirements.in chỉ ghi dependency trực tiếp
echo "litellm>=1.50" > requirements.in
echo "fastapi" >> requirements.in
# Cooldown: chỉ chấp nhận package publish trước 7 ngày
CUTOFF=$(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ)
uv pip compile requirements.in \
--generate-hashes \
--exclude-newer "$CUTOFF" \
-o requirements.txt
# Trong CI / Dockerfile: bắt buộc hash, không tự resolve thêm gì
pip install --require-hashes --no-deps -r requirements.txt
# Chặn sdist để không có setup.py nào được chạy
pip install --only-binary=:all: --require-hashes -r requirements.txt
Layer 2: Scan .pth and strange behavior in installed environments
Strict pinning only protects you from now on. What about machines that have already installed it? The script below scans every .pth file in site-packages and flags import lines with suspicious signs. Normally, only a few valid packages use .pth with imports (e.g., distutils-precedence.pth from setuptools, or files from editable installs), so the list will be short and easy to review.
Lớp 2: Tự quét .pth và hành vi lạ trong môi trường đã cài
Pin chặt chỉ bảo vệ từ nay về sau. Còn những máy đã lỡ cài thì sao? Script dưới đây quét mọi file .pth trong site-packages và đánh dấu các dòng import có dấu hiệu đáng ngờ. Bình thường chỉ vài package hợp lệ dùng .pth có import (ví dụ distutils-precedence.pth của setuptools, hoặc file của editable install), nên danh sách sẽ ngắn và dễ review.
Layer 3: Limit damage if you get hit
The worst-case assumption is that at some point you will install a malicious package. The question is how much it can take. Some principles my team applies:
- Separate the install step from the step with secrets. In GitHub Actions, only inject env secrets into the deploy step, not at the job level. The
pip installstep does not need to seeAWS_SECRET_ACCESS_KEY. - Use OIDC instead of long-lived keys. GitHub Actions and GitLab CI both support exchanging OIDC tokens for AWS/GCP credentials that only live for 15–60 minutes. If leaked, they expire quickly.
- Block egress during build. Build Docker with
--network=noneafter downloading wheels viapip download, or use an internal proxy (devpi, Artifactory) that only allows the PyPI host.
Lớp 3: Giới hạn thiệt hại khi lỡ dính
Giả định xấu nhất là kiểu gì cũng sẽ có lúc bạn cài phải package độc. Câu hỏi là nó lấy được bao nhiêu. Vài nguyên tắc team mình áp dụng:
- Tách bước install ra khỏi bước có secret. Trong GitHub Actions, chỉ inject env secret vào step deploy, không đặt ở cấp job. Bước
pip installkhông cần thấyAWS_SECRET_ACCESS_KEY. - Dùng OIDC thay cho long-lived key. GitHub Actions, GitLab CI đều hỗ trợ đổi OIDC token lấy credential AWS/GCP chỉ sống 15–60 phút. Có bị lộ thì cũng nhanh hết hạn.
- Chặn egress lúc build. Build Docker với
--network=nonesau khi đã tải wheel về bằngpip download, hoặc dùng proxy nội bộ (devpi, Artifactory) chỉ cho phép host PyPI.