
A .pth File, 34KB of Base64, and Every Secret You Have
pip install litellm. That's it. That's the exploit.
On March 24, 2026, security researchers discovered that litellm==1.82.8 on PyPI contained a 34,628-byte file called litellm_init.pth. It was a credential stealer. Not one that runs when you import litellm. One that runs every time the Python interpreter starts. Any Python interpreter. Any script. Any test. Any build.
Version 1.82.7 was also compromised — the payload was embedded directly in proxy/proxy_server.py. Two versions, two delivery mechanisms, same objective: harvest everything and exfiltrate it.
Five days ago, we wrote about the Trivy GitHub Action compromise — 75 poisoned tags, same credential harvesting playbook, same encryption scheme. We showed how cilock catches that pattern across three detection layers. Now the same pattern has hit a PyPI package with over 30 million downloads.
This is not a coincidence. This is a playbook.
Why .pth Is Worse Than You Think
Most developers know about setup.py attacks — malicious code in the install script. Package managers have gotten better at sandboxing those. The .pth mechanism is different and less understood.
Any file ending in .pth placed in site-packages/ is processed by Python on every interpreter startup. Lines starting with import are executed as code. This is a legitimate Python feature for configuring paths — and a perfect persistence mechanism for an attacker.
Key insight: The malicious code runs without any import litellm statement. If litellm is installed in your environment, every Python process — pytest, Django, Flask, Jupyter, Celery workers, CI build scripts — silently executes the stealer. You don't even need to use the library.
The Attack

Four stages. Same playbook as Trivy. Different delivery vector.
Stage 1: Package Poisoning
The attacker compromised LiteLLM's PyPI publishing credentials (likely through a compromised CI/CD pipeline or maintainer account) and pushed versions 1.82.7 and 1.82.8 with embedded payloads. In 1.82.7, the payload lived in proxy/proxy_server.py. In 1.82.8, they upgraded to the .pth mechanism — broader reach, harder to spot in code review.
Stage 2: .pth Bootstrap
The litellm_init.pth file contained a double base64-encoded Python script. On every Python startup:
# What Python's site.py does with .pth files:
# 1. Reads every .pth file in site-packages/
# 2. Lines starting with "import" are exec()'d
# 3. This happens BEFORE your script runs
# litellm_init.pth (simplified):
import base64; exec(base64.b64decode(base64.b64decode(
"VVZSS1IxUXlSWGhpUkZKb1VtMW9jMWxyWkZOT1..." # 34KB
)))Double base64 — the kind of encoding depth that makes grep-based detection miss it, but not cilock's recursive decoder.
Stage 3: Credential Harvesting
The decoded payload swept the entire machine:
- System info — hostname, OS, network interfaces, running processes
- All environment variables — API keys, database URLs, cloud tokens
- SSH keys —
~/.ssh/* - Git credentials —
~/.git-credentials,.gitconfig - Cloud credentials — AWS (
~/.aws/), GCP (~/.config/gcloud/), Azure (~/.azure/) - Kubernetes configs —
~/.kube/config - Docker configs —
~/.docker/config.json - Shell history — commands that often contain passwords and tokens
- Crypto wallets — Solana, Ethereum keystores
- SSL private keys and database credentials
- CI/CD secrets and webhook URLs
The file access pattern is identical to the Trivy attack. The filesystem paths are the same. The only difference is the entry point — .pth instead of a GitHub Action entrypoint script.
Stage 4: Encryption & Exfiltration
Harvested data was AES-256-CBC encrypted with a random session key. The session key was RSA-encrypted with a hardcoded 4096-bit public key. The bundle was HTTPS POST'd to models.litellm.cloud — a typosquat of the legitimate litellm.ai domain.
Same encryption scheme as the Trivy attack. AES-256-CBC + RSA-4096. Same typosquat exfiltration strategy. Same playbook, different package registry.
The Pattern
Two attacks in five days. Different vectors — GitHub Actions and PyPI — but structurally identical execution:
| Stage | Trivy (March 19) | LiteLLM (March 24) |
|---|---|---|
| Compromise | Force-pushed 75 Git tags | Poisoned PyPI package |
| Payload hiding | Single base64 in shell script | Double base64 in .pth file |
| Harvesting | /proc environ, runner memory, 50+ paths | Env vars, SSH, cloud creds, k8s, Docker, wallets |
| Encryption | AES-256-CBC + RSA-4096 | AES-256-CBC + RSA-4096 |
| Exfiltration | HTTPS POST to typosquat domain | HTTPS POST to typosquat domain |
When the encryption scheme, the exfiltration pattern, and the credential targets are this similar, you're not looking at two independent attackers. You're looking at a toolkit.
How cilock Catches This in CI/CD

The same three layers we demonstrated against the Trivy attack apply here. The detection logic doesn't change because the delivery vector changed — the credential harvesting behavior is identical.
Layer 1: Prevention — Package Integrity
For GitHub Actions, cilock enforces an approved action catalog with SHA pinning. The same concept extends to packages: pin dependencies to known-good hashes and enforce it with policy.
package cilock.verify
import rego.v1
# Enforce pip install --require-hashes
deny contains msg if {
some step in input.steps
step.command == "pip install"
not step.flags["require-hashes"]
msg := "pip install must use --require-hashes"
}
# Verify lock file integrity
deny contains msg if {
input.lockfile_hash != input.expected_lockfile_hash
msg := sprintf(
"Lock file modified: got %s, expected %s",
[input.lockfile_hash, input.expected_lockfile_hash]
)
}The principle is the same: don't trust the registry. Pin to known-good content hashes. Let policy enforcement reject anything that doesn't match. If LiteLLM's 1.82.8 wheel hash changed from what your lock file expected, the build stops.
Layer 2: Content Detection — Double Base64 Is Not Deep Enough
The LiteLLM attacker used double base64 encoding to hide the payload. That's exactly the kind of encoding cilock's secretscan attestor is built to unpack. It recursively decodes through multiple layers — base64, hex, URL-encoded — and runs Gitleaks pattern matching at each depth.
// scanner.go — recursive multi-layer decode
// This is the same code that caught the Trivy attack payload.
// Double base64? Decoded at depth 2. Triple? Depth 3.
if currentDepth < a.maxDecodeLayers {
for _, scanner := range defaultEncodingScanners {
candidates := scanner.Finder(contentStr)
for _, candidate := range candidates {
decodedBytes, err := scanner.Decoder(candidate)
if err == nil && len(decodedBytes) >= minSensitiveValueLength {
recursiveFindings, _ := a.scanBytes(
decodedBytes, sourceIdentifier, detector,
processedInThisScan, currentDepth+1,
)
findings = append(findings, recursiveFindings...)
}
}
}
}Against the LiteLLM payload: The .pth file's double base64 payload would be decoded at depth 1, revealing the inner base64. Decoded again at depth 2, the credential harvesting script is exposed. Gitleaks patterns match API keys, private keys, and cloud credentials in the decoded output. --attestor-secretscan-fail-on-detection blocks the build.
Layer 3: Behavioral Detection — The Filesystem Access Pattern Is a Fingerprint
Content detection catches what the payload contains. Behavioral detection catches what it does. The LiteLLM stealer reads ~/.ssh/, ~/.aws/, ~/.kube/config, ~/.docker/config.json. That access pattern is the same fingerprint cilock's trace + OPA behavioral policy already flags.
package cilock.verify
import rego.v1
# Credential harvesting pattern: multiple sensitive directories
# accessed by a single process
sensitive_paths := [
"/home/", "/.ssh/",
"/.aws/", "/.config/gcloud/", "/.azure/",
"/.kube/config", "/.docker/config.json",
"/.git-credentials",
]
credential_access_count(proc) := count {
some file in object.keys(proc.openedfiles)
some path in sensitive_paths
contains(file, path)
count := count + 1
}
deny contains msg if {
some proc in input.processes
credential_access_count(proc) >= 3
msg := sprintf(
"Credential sweep: %s (PID %d) accessed %d+ sensitive paths",
[proc.program, proc.processid, 3]
)
}The key insight: No legitimate pip install or pytest process reads SSH keys, AWS credentials, and Kubernetes configs. A single process touching 3+ credential directories is a signal with essentially zero false positive rate in a CI/CD pipeline.
The attack vector changed. The behavior didn't. That's why behavioral detection works.
With and Without cilock

Without cilock
pip install litellmpulls compromised package- .pth file installs silently — no import needed
- Next Python process triggers the stealer
- SSH keys, cloud creds, k8s tokens harvested
- Encrypted bundle sent to attacker's domain
- No forensic trail. Thousands of organizations exposed.
With cilock
- Hash-pinned lock file rejects unexpected package content
- Secretscan decodes double base64 and finds credential patterns
- Trace + OPA catches the credential sweep filesystem access
- Build blocked with signed attestation of what happened
- Forensic evidence: exactly which files were accessed, by which process
- Policy enforcement prevents release of compromised artifacts
The Honest Gap: CI/CD vs. Everywhere Else
cilock operates in CI/CD pipelines. The LiteLLM .pth file runs on any Python interpreter — developer laptops, production servers, Jupyter notebooks, data science environments. cilock catches this in CI builds, test runs, and deployment pipelines. It does not protect your local python3 script.py.
That said, CI/CD is where this matters most:
- CI runners have the highest-value credentials. Cloud provider tokens, deployment keys, registry credentials, signing keys — the secrets that unlock production infrastructure.
- CI is where packages are installed from scratch. Developer machines may have cached older, clean versions. CI pulls fresh every build.
- CI is the blast radius multiplier. One compromised GitHub Actions runner can leak credentials for every service the pipeline deploys to.
Protecting CI/CD doesn't protect everything. But it protects the thing that has the keys to everything.
What to Do Right Now
If you had litellm 1.82.7 or 1.82.8 installed anywhere:
- Check for the .pth file:
find $(python3 -c "import site; print(site.getsitepackages()[0])") -name "litellm_init.pth" - Rotate everything. SSH keys, cloud credentials, API tokens, database passwords, signing keys. Every secret on any machine where either version was installed. Not just the ones you think were used — the stealer takes everything it can read.
- Audit your CI runners. Check which pipelines installed litellm. Those runners' secrets are compromised. Rotate service account credentials, deployment tokens, and registry auth.
- Pin your dependencies. Use
pip install --require-hasheswith a lock file. This won't help after the fact, but it's the first layer of defense against the next one.
The supply chain attack playbook is repeating. The question is whether your CI/CD pipeline is watching.
cilock is open source. Add it to your pipeline today.
github.com/aflock-ai/cilock-action
Read the original Trivy analysis: 75 Poisoned Tags and Nobody Noticed
We're at RSAC 2026 — NXT-1, Early Stage Expo. We'll run cilock on your pipeline live. Bring your repo URL.
Or start now at testifysec.com