Skip to content

Instantly share code, notes, and snippets.

@harupy
Last active March 25, 2026 13:06
Show Gist options
  • Select an option

  • Save harupy/f02fe115ffcf534283bdd622652e63fd to your computer and use it in GitHub Desktop.

Select an option

Save harupy/f02fe115ffcf534283bdd622652e63fd to your computer and use it in GitHub Desktop.

Is exclude-newer Vulnerable to Supply Chain Attacks?

A deep dive into how uv's exclude-newer handles PyPI supply chain attack vectors, with reference to the LiteLLM/TeamPCP incident (March 2026).

Background: The LiteLLM Attack (March 24, 2026)

TeamPCP compromised the Trivy GitHub Action, which exfiltrated LiteLLM's PYPI_PUBLISH token from CI. They then published two malicious versions directly to PyPI:

Version Time (UTC) Technique Trigger
1.82.7 10:39 Payload in proxy_server.py import litellm.proxy
1.82.8 10:52 litellm_init.pth in site-packages Every Python startup — no import needed

The .pth file in 1.82.8 was particularly dangerous: Python auto-executes .pth files on interpreter startup, meaning even pip install, python -c, or an IDE's language server would trigger the credential stealer.

How exclude-newer Works (from uv source code)

uv's exclude-newer checks the per-file upload timestamp (via PEP 700's upload-time field) for each wheel/sdist on PyPI.

From crates/uv-resolver/src/version_map.rs:

let (excluded, upload_time) = if let Some(exclude_newer) = &self.exclude_newer {
    match file.upload_time_utc_ms.as_ref() {
        // File uploaded AFTER cutoff → EXCLUDE
        Some(&upload_time) if upload_time >= exclude_newer.timestamp_millis() => {
            (true, Some(upload_time))
        }
        // File has NO upload time → EXCLUDE (conservative)
        None => {
            (true, None)
        }
        // File uploaded BEFORE cutoff → ALLOW
        _ => (false, None),
    }
} else {
    (false, None)
};

Key behaviors:

Scenario Result
upload_time >= exclude_newer Excluded
upload_time < exclude_newer Allowed
upload_time missing Excluded (conservative)
exclude_newer not set All allowed

Attack Vectors vs exclude-newer

1. New Malicious Version (e.g., LiteLLM 1.82.7/1.82.8)

Protected? Yes.

If UV_EXCLUDE_NEWER=2026-03-10T00:00:00Z, the malicious versions (uploaded March 24) are blocked because their upload time exceeds the cutoff.

2. Build Tag Shadowing

An attacker with maintainer access uploads a higher build tag to an existing version:

package-1.0.0-py3-none-any.whl        # original, uploaded 2025-01-01
package-1.0.0-1-py3-none-any.whl      # attacker's, uploaded 2026-03-24

pip would prefer the higher build tag. But uv checks per-file upload time, so the new wheel is excluded independently of the version number.

Protected? Yes — the new file's upload time is after the cutoff.

3. Platform-Specific Wheel Injection

An attacker uploads a more specific wheel to shadow the generic one:

package-1.0.0-py3-none-any.whl        # original, uploaded 2025-01-01
package-1.0.0-cp310-none-any.whl      # attacker's, uploaded 2026-03-24

pip/uv would normally prefer the more specific cp310 wheel.

Protected? Yes — same reason, per-file upload time check.

4. Compromised Index / MITM Serving a Different File

If the index itself is compromised and serves a different file with a spoofed upload time (claiming it was uploaded long ago):

Protected? No. exclude-newer trusts the upload-time field from the index. If the index lies, the check is bypassed. This is where lockfile hash pinning (uv sync --locked) provides the real defense.

5. Attacker Uploads Before the Cutoff Date

If the attacker uploads malicious code before the exclude-newer cutoff (e.g., a sleeper attack), the timestamp check won't help.

Protected? No. This is a time-of-check problem — exclude-newer only protects against packages uploaded after the cutoff.

6. Missing upload-time Field (Non-PyPI Index)

PyPI provides upload-time for all packages (PEP 700). But private registries may not.

Protected? Yes, conservatively — uv excludes files with missing upload times when exclude-newer is set, and warns the user.

Summary

Attack Vector exclude-newer Protects? Notes
New malicious version ✅ Yes Blocked by upload timestamp
Build tag shadowing ✅ Yes Per-file timestamp check
Platform wheel injection ✅ Yes Per-file timestamp check
Compromised/MITM index ❌ No Spoofed upload-time bypasses check
Sleeper attack (pre-cutoff) ❌ No Already within the allowed window
Missing upload-time ✅ Yes Conservative: excluded by default

Defense in Depth

No single mechanism is sufficient. The recommended stack:

  1. uv.lock with hash pinning — rejects any file not matching locked SHA256 hashes
  2. uv sync --locked — refuses to install if lockfile is stale
  3. exclude-newer / dependency cooldowns — time-based filter blocks very recent uploads
  4. Review lockfile diffs in PRs — new hashes for unchanged versions are a red flag
  5. PyPI Trusted Publishers (OIDC) — limits who can upload, tied to CI identity
  6. 2FA on PyPI accounts — reduces account takeover risk

References

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment