Skip to content

How to Protect Against Python Supply Chain Attacks with uv

Malicious packages uploaded to PyPI are usually pulled within hours or days, either yanked or deleted outright. A dependency cooldown tells uv to ignore packages published within a recent window, holding a compromised release out of your resolution long enough for someone else to catch it.

Set a dependency cooldown

The exclude-newer setting accepts durations and timestamps. Security hardening wants a duration, which creates a rolling window that moves forward with the current date. Rolling the cutoff forward with a duration covers every value the option takes.

On the command line

uv lock --exclude-newer "7 days"
uv sync --exclude-newer "7 days"

This form is a one-off. uv does not store it, so the next plain uv sync re-resolves without the cooldown. Use configuration for a cooldown you expect to hold.

In project configuration

Add the setting to pyproject.toml or uv.toml so it applies to every uv lock and uv sync in the project:

# pyproject.toml
[tool.uv]
exclude-newer = "7 days"
# uv.toml
exclude-newer = "7 days"

Use one or the other. A uv.toml beside pyproject.toml makes uv ignore [tool.uv] entirely, so a cooldown set there stops applying.

In user-level configuration

Apply the cooldown to all projects on a machine by adding it to the user-level uv.toml:

# ~/.config/uv/uv.toml
exclude-newer = "7 days"

Enforce the cooldown in CI

Keep the cooldown in configuration rather than a pipeline-only flag, so developers and CI resolve against the same window. Then install from the lockfile:

uv sync --locked

--locked fails the job when pyproject.toml and uv.lock disagree, catching a dependency added without a fresh uv lock:

$ uv sync --locked
Resolved 3 packages in 2ms
error: The lockfile at `uv.lock` needs to be updated, but `--locked` was provided.

hint: To update the lockfile, run `uv lock`.

A rolling window does not trip this check by itself. uv records the span in uv.lock rather than the timestamp it resolved to, so uv sync --locked and uv lock --check both pass on a freshly locked project.

Let an urgent patch through the cooldown

A cooldown delays security fixes along with everything else. When the patch you need is younger than the window, exempt that one package with exclude-newer-package while the rest of the tree keeps its buffer:

[tool.uv]
exclude-newer = "7 days"
exclude-newer-package = { my-package = false }   # install my-package regardless of age

Each exemption trades the buffer for speed on one package, so remove it once the window catches up. Overriding the cutoff for one package covers the other values the field takes.

Choose a cooldown period

7 days is a practical default. Most malicious uploads are detected within this window, and most projects can wait a week for new dependency versions. The litellm supply chain attack in March 2026 was discovered and yanked within hours, so a 7-day buffer would have been more than enough.

30 days suits production deployments and internal tooling where stability matters more than access to recent releases.

Diagnose cooldown failures

Read past the solver error

When the cooldown excludes every candidate, the solver error reads like an ordinary version conflict, and only the hint below it names exclude-newer as the cause. Read past the No solution found block before assuming the constraint is wrong. Reading that hint covers the output in full.

Check whether the index publishes upload times

The cooldown needs the upload-time field defined in PEP 700, which PyPI publishes and many private indexes do not. Exempting an index that does not publish upload times shows the failure and the fix.

Tip

A cooldown protects the moment when new versions enter the dependency tree. A lockfile pins the exact versions that were resolved. Neither one helps against a malicious version already pinned in the lockfile; uv audit scans what is already locked.

Block known malware at install time

A cooldown buys time for malicious uploads to be detected. uv’s malware check acts on the detections themselves. With the check enabled, every command that syncs the environment (uv add, uv sync, and others) looks up the locked packages against the MAL advisories in the OSV database, sourced from the OpenSSF malicious-packages project. A match stops the sync before the package installs, so none of its code runs.

In project configuration

In pyproject.toml, add [tool.uv.audit]; in uv.toml, add [audit] to enable the check for every sync in the project:

# pyproject.toml
[tool.uv.audit]
malware-check = true
# uv.toml
[audit]
malware-check = true

To query a custom advisory database instead of the default OSV endpoint, add malware-check-url:

# pyproject.toml
[tool.uv.audit]
malware-check = true
malware-check-url = "https://your-internal-advisory-db.example.com"

With an environment variable

For CI pipelines or one-off sessions, set UV_MALWARE_CHECK instead:

export UV_MALWARE_CHECK=1

With the check enabled, uv prints an experimental-feature warning on every sync. A clean lookup adds nothing else, so an ordinary install means nothing matched:

$ uv sync
Resolved 2 packages in 2ms
warning: Malware checks are experimental and may change without warning. Pass `--preview-features malware-check` to disable this warning.
Installed 1 package in 4ms
 + six==1.17.0

The check only blocks malware already recorded as an advisory. The cooldown covers the gap between a malicious upload and its advisory, so run both.

Learn More

Last updated on