critical

CVE

Not assigned

CWE

CWE-295

Affected Surface

  • `uv` versions earlier than `0.13.4` from PyPI, standalone installers, or pinned binaries
  • Developer workstations, CI runners, and container images that used `uv`'s default bundled Mozilla root certificates to reach PyPI, `--index-url` mirrors, or other HTTPS package hosts
  • Environments that replaced `uv`'s defaults with `SSL_CERT_FILE`, `SSL_CERT_DIR`, or system certificates if those trust stores still included the compromised emSign lineage

If you use uv as a package installer, lockfile resolver, or CI bootstrap tool, the 20 October advisory is worth reading as a supply-chain issue, not just a certificate story. The trust boundary at stake is the HTTPS session uv uses to download package indexes, wheels, source distributions, and Python installs. Before 0.13.4, that connection trusted a vendored root bundle that still contained emSign ECC Root CA - G3, even after the associated intermediate keys had been reported compromised and revoked.

For AppSec teams, the abuse path is straightforward:

attacker holds one compromised intermediate key
  -> presents forged certificate for pypi.org, files.pythonhosted.org, or an internal index
  -> uv accepts the TLS chain because the bundled trust store still contains the emSign root
  -> attacker serves tampered package metadata or artifacts over an otherwise "valid" HTTPS session

No preinstall hook or maintainer-account takeover is required if the attacker can win the network path. That puts this in the same family of delivery-path integrity problems as our earlier coverage of apko and melange trusting the wrong APK proof and Go’s checksum trust gap around GOMODPROXY. The failure lives in what the tool decides to trust before it installs software.

Affected versions and what changed

The version boundary is clean:

uv < 0.13.4   affected
uv >= 0.13.4  fixed

The upstream advisory, package metadata, and 0.13.4 release all point to the same fix line. The important part is that uv did not ship a bespoke certificate parser bug. It inherited a stale trust anchor through its bundled Mozilla roots, then corrected that bundle one day after webpki-root-certs published 1.0.10 with the emSign removal.

The release commit for uv 0.13.4 is mostly version churn, but the real security change sits in the preceding dependency update:

name = "webpki-root-certs"
-version = "1.0.9"
+version = "1.0.10"

The linked webpki-roots release note is unusually direct:

Removed "emSign ECC Root CA - G3"

That is the trust decision that mattered.

Why vulnerable uv builds used the bad root by default

The CLI definition says the quiet part plainly. In crates/uv-cli/src/lib.rs, the --system-certs flag exists because the default is not the platform store:

/// Whether to load TLS certificates from the platform's native certificate store [env: UV_SYSTEM_CERTS=]
///
/// By default, uv uses bundled Mozilla root certificates, which improves portability and
/// performance (especially on macOS).
pub system_certs: bool,

The HTTP client builder wires that default into the request path. In crates/uv-client/src/base_client.rs, once uv chooses Rustls it decides between three certificate sources: explicit overrides, platform roots, or its vendored bundle.

let client_builder = if let Some(custom_certs) = custom_certs {
    client_builder.tls_certs_only(custom_certs)
} else if self.system_certs {
    client_builder
} else {
    client_builder.tls_certs_only(Certificates::webpki_roots().to_reqwest_certs())
};

And Certificates::webpki_roots() in crates/uv-client/src/tls.rs is only a thin wrapper around the vendored crate:

pub(crate) fn webpki_roots() -> Self {
    Self(webpki_root_certs::TLS_SERVER_ROOT_CERTS.to_vec())
}

That means the security story is not hidden in a corner case. For the default uv path, the vendored root list is the trust store. Updating webpki-root-certs changes what every HTTPS package fetch will accept.

Why this matters for package delivery

uv sits on several sensitive edges at once:

  • it resolves packages from PyPI and alternate indexes
  • it downloads wheels and source distributions
  • it is often bootstrapped into CI with pinned binaries or curl-based install steps
  • it is increasingly used as the default Python package tool inside container builds

If an attacker can impersonate a package host that uv is allowed to contact, they do not need to break your application. They need the package manager to accept the TLS session long enough to hand it different package contents or metadata.

That can affect:

  • uv pip install ...
  • uv sync
  • uv lock
  • uvx ...
  • Python downloads and tool installs that reuse the same HTTP client stack

Teams sometimes hear “certificate issue” and think “browser warning.” That framing is too weak here. This was a package-ingestion trust problem in a tool that many organizations use before code review, tests, or signature checks ever happen.

Custom certs and system certs are not a magic exemption

The advisory is careful on this point, and responders should be too.

If you already ran uv with --system-certs, the OS trust store rather than the vendored bundle decided whether the bad lineage was trusted. That can reduce exposure, but only if the system store had already distrusted the emSign line.

If you used SSL_CERT_FILE or SSL_CERT_DIR, uv treats those as a full replacement for its default roots. The code says so explicitly:

/// Returns `None` if neither variable is set to a non-empty value. An explicitly configured
/// file or directory always replaces the default certificate roots, even when it is missing,
/// inaccessible, or contains no valid certificates.
pub fn from_env() -> Option<Self> { ... }

So the response question is not just “Did we set custom certs?” It is “What exact roots did that file, directory, or operating system trust on 20 October?”

What to search for

Start by finding where uv is part of the build or bootstrap path:

rg -n 'astral\\.sh/uv|\\buvx?\\b|uv pip|uv sync|uv lock|UV_SYSTEM_CERTS|SSL_CERT_FILE|SSL_CERT_DIR' \
  .github Dockerfile docker-compose.yml compose.yml scripts .devcontainer .

Then check the actual binary or package version on runners and base images:

uv --version
python -m pip show uv 2>/dev/null || true

If you build containers or golden CI images, also search image definitions and install scripts for pinned uv versions below 0.13.4:

rg -n 'uv==0\\.|uv 0\\.|0\\.13\\.[0-3]|install\\.sh|astral-sh/uv/releases/download' \
  Dockerfile* .github scripts ci .

The scoping questions that matter are:

  1. did any workstation, runner, or image use uv below 0.13.4?
  2. did that system fetch packages through the default bundled roots rather than a known-good system store?
  3. could an attacker influence the network path to PyPI, an artifact mirror, or a private index during that period?

Remediation

Upgrade first:

  1. move every uv install path to 0.13.4 or later
  2. rebuild CI and developer base images that embed older uv binaries
  3. if you rely on custom trust roots, review the actual contents of SSL_CERT_FILE, SSL_CERT_DIR, or the system store instead of assuming the override made you safe

Then widen the lens. The uv advisory explicitly points out that this was not unique to one package. Other software that vendors CA bundles from projects like certifi or webpki-root-certs may have carried the same stale trust anchor. The Python security announcement for certifi went further and told operators to rebuild container images so the updated CA material actually lands on disk. The same operational advice makes sense here if uv is baked into your builder images.

What defenders should remember

The useful lesson in this incident is that package-manager trust does not begin at the artifact hash. It begins earlier, at the TLS session that decides which index page, wheel, or source distribution the tool will accept in the first place.

For uv, the fix was small: one dependency bump and a new release. The exposure was broader than that diff looks, because the old default trust store sat in front of every HTTPS package fetch. If your Python supply chain now depends on uv, this is the kind of issue that deserves the same attention as a malicious package publish. It changes what code your environment can be convinced to download and trust.

From research to remediation

Check whether this pattern exists in your codebase

Turn this research into a remediation workflow. Scan dependencies and package manifests for similar supply-chain risk, then prioritize fixes with reachability context.

References