Like putting a recall on a car part that was discontinued two models ago
CVE-2025-47273 is a path traversal in the PackageIndex.download() method of Python's setuptools library, present in all versions prior to 78.1.1. The bug is textbook os.path.join() abuse: when the filename extracted from a URL begins with / (e.g., %2froot%2f.ssh%2fauthorized_keys), Python's os.path.join(tmpdir, name) silently discards the tmpdir prefix and returns the absolute path. The existing sanitization only stripped .. sequences, missing this well-known class of path injection entirely. Affected versions span the entire setuptools lineage through 78.1.0; the fix landed in 78.1.1 on April 19, 2025. Distro backports exist for Debian bullseye (52.0.0-4+deb11u2), bookworm (66.1.1-1+deb12u2), Ubuntu focal (45.2.0-1ubuntu0.3), and jammy (59.6.0-1.2ubuntu0.22.04.3). Notably, the entire package_index module was removed from setuptools in v80.3.0 (May 2025).
The vendor CVSS of 8.8 dramatically overstates real-world risk. The vulnerable function lives inside setuptools.package_index, a module that powered the long-deprecated easy_install command — replaced by pip over a decade ago. No modern Python workflow, CI pipeline, or package manager invokes PackageIndex.download(). Exploitation requires the attacker to either (a) trick application code into calling this specific deprecated API with an attacker-controlled URL, or (b) get a victim to run easy_install against a malicious package index — both scenarios that are vanishingly rare in 2026. Ubuntu's own security team rated this Medium priority, and Ubuntu 24.04+ and 26.04+ are marked 'Not affected' because their shipped setuptools versions already removed or patched the module. The CVSS vector's AC:L (low attack complexity) is misleading; the real-world prerequisites add substantial friction that the score ignores.
4 steps from start to impact.
Attacker hosts malicious payload server
%2froot%2f.ssh%2fauthorized_keys. This is trivial to set up — the PoC uses a simple Python HTTP server.- Attacker controls an HTTP endpoint reachable from the target
- Outbound HTTP to arbitrary hosts is blocked by egress filtering in most enterprise environments
Victim code calls PackageIndex.download() with attacker URL
setuptools.package_index.PackageIndex().download(attacker_url, tmpdir). This is the critical gate. In practice, this means either: the target runs easy_install against a malicious index page, or custom application code explicitly uses the deprecated PackageIndex API with attacker-influenced input. Neither scenario is common in modern deployments.- Target has setuptools < 78.1.1 installed
- Target code explicitly calls the deprecated PackageIndex.download() API or uses easy_install
- Attacker can influence the URL argument
- easy_install has been deprecated since 2013 and removed in setuptools 80.3.0 (May 2025)
- pip is the standard installer and does NOT use PackageIndex.download()
- Almost no modern Python code calls this API programmatically
- The attacker essentially needs local code execution context to trigger the function call
PackageIndex / easy_install in codebase) reveals exposureos.path.join drops tmpdir, writes to absolute path
PackageIndex._download_url() processes the crafted URL, it extracts the filename via URL parsing. The existing sanitization only replaces .. with . but fails to catch absolute paths starting with /. Python's os.path.join('/tmp/download', '/root/.ssh/authorized_keys') returns /root/.ssh/authorized_keys, discarding the tmpdir entirely. The downloaded payload is written to this attacker-controlled path.- The process has write permissions to the target filesystem path
- Non-root processes cannot write to /root/.ssh/ or other privileged paths
- Containerized workloads limit writable filesystem surface
- SELinux/AppArmor policies may block writes outside expected paths
Attacker leverages written file for code execution
PackageIndex.download().- Written file is in a location that triggers execution (SSH keys, cron, etc.)
- Process ran with sufficient privileges to write to that location
- SSH key injection requires the process to run as root AND sshd to be configured for key auth
- Cron/systemd injection requires root or appropriate group membership
- In CI/CD containers, SSH is typically not running
.ssh/, /etc/cron.d/, or systemd pathspip install --upgrade setuptools>=78.1.1 in every virtualenv, container image, and system Python. For the noisgate MEDIUM remediation SLA, complete within 365 days. Prioritize CI/CD build images and any system where custom code imports setuptools.package_index.package_index and easy_install completely. This eliminates the attack surface rather than just patching it. Recommended for environments that can tolerate the breaking change (any code importing setuptools.package_index will fail). No mitigation SLA for MEDIUM — go straight to the 365-day remediation window.grep -rn 'PackageIndex\|easy_install\|package_index' across your repositories and deployed code. If no hits, this CVE has zero exploitable surface in your environment regardless of setuptools version. This takes hours, not days, and immediately tells you whether to prioritize the upgrade.- WAF / network IDS — This is not a network service vulnerability. The exploit happens inside a local Python process calling a library function. No network signature will catch it.
- Container isolation alone — While containers limit filesystem scope, if the process writes to a mounted volume or the container runs privileged, the traversal still reaches the host. Containers reduce blast radius but don't fix the bug.
- Pinning to a requirements.txt without upgrading — Pinning setuptools to the current vulnerable version locks in the bug. Pin to
setuptools>=78.1.1instead.
The supporting signals.
| In-the-Wild Exploitation | No evidence. Not listed on CISA KEV. No GreyNoise or Shadowserver observations. No known campaigns targeting this CVE. |
|---|---|
| Proof-of-Concept | Public. ~40 GitHub repos (mostly clones). Primary PoC: ahmedreda38/CVE-2025-47273-PoC demonstrates SSH key injection via a crafted URL. Trivial to reproduce in a lab but requires the victim to call the deprecated API. |
| EPSS | 1.592% (75th percentile) — low probability of exploitation in the next 30 days, consistent with the limited attack surface. |
| KEV Status | Not listed. No CISA KEV entry as of 2026-10-14. |
| CVSS Vector | CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H (8.8). CVSS 4.0 scored at 7.7. The AC:L is misleading — reaching the vulnerable code path requires calling a deprecated, rarely-used API. |
| Affected Versions | All setuptools versions < 78.1.1. The vulnerable package_index module was entirely removed in setuptools 80.3.0 (May 3, 2025). |
| Fixed Versions | Upstream: 78.1.1+ (also 75.3.4 backport). Debian: bullseye 52.0.0-4+deb11u2, bookworm 66.1.1-1+deb12u2. Ubuntu: focal 45.2.0-1ubuntu0.3, jammy 59.6.0-1.2ubuntu0.22.04.3. Ubuntu 24.04+ and 26.04+ are not affected. |
| Scanning / Exposure | Not a network service — Shodan/Censys/GreyNoise are not applicable. Exposure is determined by SBOM / dependency scanning for setuptools < 78.1.1 AND usage of PackageIndex API. |
| Disclosure | Reported by jaraco (setuptools maintainer) on 2025-04-14 (GitHub #4946). Fix merged 2025-04-19. CVE published 2025-05-17. Related to prior setuptools CVEs: CVE-2022-40897, CVE-2024-6345. |
| Distro Priority | Ubuntu rated Medium. Ubuntu 24.04 LTS and 26.04 LTS marked Not affected. |
Sources.
- GitHub Security Advisory GHSA-5rjg-fvgr-3xxf
- setuptools Issue #4946 — Original vulnerability report
- PoC: ahmedreda38/CVE-2025-47273-PoC
- Ubuntu CVE Tracker — CVE-2025-47273
- Debian Security Tracker — CVE-2025-47273
- NVD — CVE-2025-47273
- setuptools deprecation history — easy_install removal
- IBM Security Bulletin — IBM Fusion / Astronomer
Why this verdict
- Deprecated and removed code path: The vulnerable
PackageIndex.download()lives insetuptools.package_index, which powered theeasy_installcommand deprecated since 2013 and entirely removed in setuptools 80.3.0 (May 2025). Any environment running setuptools ≥ 80.3.0 has zero attack surface. This alone invalidates the vendor'sAC:Lassumption. - No passive attack surface: Unlike a network service vulnerability, this bug cannot be triggered remotely against a running application. The attacker must cause victim code to call a specific deprecated function with an attacker-controlled URL — this requires either prior code execution (at which point you don't need a path traversal) or the victim to voluntarily run
easy_installagainst a malicious index (a scenario that barely exists in 2026). - EPSS and exploitation evidence confirm low risk: EPSS at 1.592% (75th percentile), no KEV listing, no in-the-wild exploitation, no GreyNoise/Shadowserver observations. Despite 40+ GitHub PoC repos, the PoCs all require the victim to explicitly invoke the deprecated API — they don't demonstrate a realistic attack chain against a standard deployment.
- Role multiplier: setuptools is installed on virtually every Python environment, but the *vulnerable code path* is exercised in essentially none of them. (a) *Workstations/dev:* setuptools present but
PackageIndex.download()unused — pip handles all installs. Risk: negligible. (b) *Application servers:* setuptools installed as build dependency,PackageIndexnever called at runtime. Risk: negligible. (c) *CI/CD pipelines (high-value role):* Modern CI usespip install— noteasy_installorPackageIndex. A legacy build script explicitly using the deprecated API would be the only vector; this represents well under 1% of CI/CD environments. Even in that case, CI runners are typically containerized with limited persistent filesystem. The blast radius does not reach supply-chain or fleet-scale compromise through this code path alone. The floor does not apply because the vulnerable function is not exercised in the high-value role's canonical workflow. - Ubuntu concurs: Ubuntu's security team independently rated this Medium priority. Ubuntu 24.04 LTS and 26.04 LTS are marked 'Not affected' — their shipped setuptools versions have the module removed.
Why not higher?
The 8.8 vendor score assumes low attack complexity and network-reachable exploitation, but the real attack chain requires the victim to call a deprecated API that essentially no modern Python workflow uses. There is no passive attack surface — this is not a service listening on a port. The vulnerable module was removed entirely in May 2025, and modern OS releases ship unaffected versions. Without evidence of in-the-wild exploitation, KEV listing, or a realistic attack chain against standard deployments, HIGH or CRITICAL would overstate the urgency and misallocate patching resources.
Why not lower?
Despite the deprecated status, setuptools < 78.1.1 remains installed on millions of older systems (Debian bullseye/bookworm, Ubuntu focal/jammy, RHEL-based distros with pinned dependencies). The arbitrary file write primitive is real and well-understood — if an application does happen to call PackageIndex.download() with external input, the path to code execution is short. Public PoCs lower the bar for exploitation if the preconditions are met. Dropping to LOW would undercount the residual risk on legacy systems with stale dependencies.
Crowdsourced verification payload.
Run on any host where you want to check exposure. Execute with python3 check_cve_2025_47273.py — no special privileges required. Works in virtualenvs and system Python.
#!/usr/bin/env python3
"""Check for CVE-2025-47273: setuptools PackageIndex path traversal.
Outputs VULNERABLE, PATCHED, or UNKNOWN.
Exit codes: 1=VULNERABLE, 0=PATCHED, 2=UNKNOWN
"""
import sys
try:
import importlib.metadata as md
except ImportError:
import importlib_metadata as md # Python < 3.8 fallback
try:
from packaging.version import Version
except ImportError:
print("UNKNOWN - 'packaging' module not installed; cannot compare versions")
sys.exit(2)
try:
ver_str = md.version("setuptools")
except md.PackageNotFoundError:
print("PATCHED - setuptools is not installed in this environment")
sys.exit(0)
ver = Version(ver_str)
# Check if the vulnerable module still exists
module_removed = False
try:
import setuptools.package_index # noqa: F401
except (ImportError, ModuleNotFoundError):
module_removed = True
if module_removed:
print(f"PATCHED - setuptools {ver_str} does not contain package_index module (removed in 80.3.0)")
sys.exit(0)
# Fixed upstream in 78.1.1; backport in 75.3.4
if ver >= Version("78.1.1"):
print(f"PATCHED - setuptools {ver_str} >= 78.1.1 (upstream fix)")
sys.exit(0)
elif ver >= Version("75.3.4") and ver < Version("76.0.0"):
print(f"PATCHED - setuptools {ver_str} (75.3.x backport branch)")
sys.exit(0)
else:
# Check for distro backports by testing the actual behavior
import os
import tempfile
try:
from setuptools.package_index import PackageIndex
pi = PackageIndex()
# Test the _download_filename resolution without network
# The fix ensures name is resolved relative to tmpdir
with tempfile.TemporaryDirectory() as tmpdir:
name = "/etc/test_traversal"
result = os.path.join(tmpdir, name)
if result == "/etc/test_traversal":
# os.path.join still vulnerable at the language level;
# check if setuptools added its own guard
import inspect
src = inspect.getsource(pi._download_url)
if "resolve" in src or "relpath" in src or "commonpath" in src:
print(f"PATCHED - setuptools {ver_str} appears to have distro backport (path validation found in source)")
sys.exit(0)
else:
print(f"VULNERABLE - setuptools {ver_str} < 78.1.1 with package_index module present")
sys.exit(1)
except Exception as e:
print(f"VULNERABLE - setuptools {ver_str} < 78.1.1 (could not confirm backport: {e})")
sys.exit(1)
print(f"UNKNOWN - setuptools {ver_str}, unable to determine patch status")
sys.exit(2)