Someone left the janitor's closet unlocked and a prankster swapped the trash bin labels so the janitor throws away the good files
CVE-2026-12345 is a TOCTOU (time-of-check-time-of-use) race condition in CPython's tempfile.TemporaryDirectory._cleanup() method. When the cleanup routine encounters permission errors, it re-resolves file paths before deletion. During that narrow re-resolution window, a local attacker who can write to the temporary directory tree can swap a subdirectory for a symbolic link pointing outside the tree. The cleanup process then follows the symlink and deletes or resets permissions on arbitrary files — with whatever privileges the victim process holds. All CPython versions prior to 3.15.0 are affected (3.10.x, 3.11.x, 3.12.x, 3.13.x, 3.14.x). This is a close relative of CVE-2023-6597 (CVSS 7.8 HIGH), which addressed an earlier incomplete fix for the same symlink-following behavior in TemporaryDirectory.
The published CVSS 4.0 base score of 5.9 MEDIUM is honest and reasonable. The attack vector is strictly local (AV:L) — there is no network-reachable entry point. The attack requires present timing conditions (AT:P), meaning the attacker must win a race. There is no confidentiality impact (VC:N); the damage is limited to integrity (file deletion, permission clobbering) and availability (data loss). Unlike CVE-2023-6597 which scored 7.8 under CVSS 3.1, this CVE uses the stricter CVSS 4.0 methodology which better accounts for the timing prerequisite. For a fleet of 10,000 hosts, the practical risk is constrained to multi-user systems where an unprivileged attacker already has shell access on the same box as a privileged Python process using TemporaryDirectory — a real but narrow scenario.
4 steps from start to impact.
Local shell access on target host
tempfile.TemporaryDirectory. This could be via SSH, a compromised web app spawning under a low-privilege user, or a shared CI runner. The attacker needs write access to the filesystem, specifically to a location where they can observe or predict the temporary directory path.- Local code execution on the target host
- Ability to enumerate or predict /tmp paths (trivial on most Linux systems)
- Requires the attacker to already be on the box — this is post-initial-access
- Cloud workloads in containers often run single-process, eliminating the multi-user scenario
Identify a privileged Python process using TemporaryDirectory
tempfile.TemporaryDirectory objects. They monitor the /tmp directory (or whatever tempdir is configured) to observe the creation of tmp* directories. On Linux, /proc and inotify make this straightforward.- A privileged process on the same host actively uses tempfile.TemporaryDirectory
- The temp directory base path is world-readable (default for /tmp)
- Many applications use
tempfile.mkdtemp()ortempfile.NamedTemporaryFile()instead ofTemporaryDirectory - Containerized Python apps rarely run as root alongside unprivileged users
Race the cleanup — swap directory for symlink
TemporaryDirectory cleanup and hits a permission error triggering re-resolution, the attacker replaces a subdirectory inside the temp tree with a symbolic link pointing to a target directory outside the tree (e.g., /etc, /var/lib/important_data). The timing window is narrow but the attacker can retry rapidly. No public exploit tool exists; this requires a custom C or Python script using inotify + rename(2).- Write access to the TemporaryDirectory tree (or its parent if permissions allow)
- The cleanup must encounter a permission error to trigger the vulnerable re-resolution code path
- The race window is narrow — requires precise timing or rapid retry loops
- The cleanup must hit the error-handling path, not the happy path
- No known weaponized tool or public PoC exists
Arbitrary file deletion or permission reset
- Successful race condition exploitation in step 3
- The victim process has write/delete permissions on the symlink target
- Impact is file destruction, not code execution — limited lateral movement value
- Destroying files on a production host is noisy and likely triggers alerts
- Attacker cannot control which specific files are deleted beyond choosing the target directory
TMPDIR or TMP environment variables to point to per-user directories (e.g., /run/user/$UID on systemd systems, or mount /tmp with nosuid and use pam_namespace for per-user /tmp isolation). This eliminates the multi-user prerequisite by ensuring unprivileged users cannot write to another user's temp tree. Deploy within the noisgate remediation SLA of 365 days for MEDIUM, or sooner if you operate shared multi-user Python hosts.TemporaryDirectory run in dedicated containers or VMs where no untrusted user has local access. This removes the local attacker prerequisite entirely. Most modern Kubernetes and Docker deployments already satisfy this. Validate your CI runner configuration — shared runners with multiple UIDs are the primary remaining risk surface. No mitigation SLA for MEDIUM — go straight to the 365-day remediation window.auditctl -w /tmp -p wa -k tmp_symlink. This provides detection coverage while waiting for the patch. Does not prevent exploitation but drastically reduces dwell time.- WAF / network IDS — This is a local vulnerability with no network component. No network-layer control can detect or prevent exploitation.
- Application-level sandboxing (seccomp, AppArmor profiles) — While seccomp could theoretically block
symlink(2), most Python application profiles do not restrict symlink creation, and doing so would break legitimate functionality in many libraries. - Disabling symlinks system-wide — Not practical on Linux/macOS. The
nosymfollowmount option exists on some kernels but breaks too many applications to deploy broadly.
The supporting signals.
| In-the-Wild Exploitation | No confirmed exploitation. Not listed in CISA KEV. Feedly reports unverified mentions on Mastodon, but no campaign or threat actor attribution. This is a local-only vulnerability with no network attack surface, making mass exploitation extremely unlikely. |
|---|---|
| Proof-of-Concept Availability | No public PoC found. Checked pocindex.io, GitHub (no repos named CVE-2026-12345), ExploitDB, and Nuclei templates — all negative. The GitHub issue #157579 describes the mechanism in sufficient detail to build a PoC, but none has been published. The predecessor CVE-2023-6597 also had no widely circulated weaponized exploit. |
| EPSS Score | Not yet scored. CVE was disclosed 2026-09-29 (1 day ago). FIRST EPSS model has not yet ingested this CVE. Expect a low score (likely <5th percentile) given local-only attack vector and no PoC. |
| CISA KEV Status | Not listed. No federal remediation deadline. Given the local attack vector and lack of exploitation evidence, KEV listing is unlikely. |
| CVSS Vector & Interpretation | CVSS:4.0/AV:L/AC:L/AT:P/PR:N/UI:N/VC:N/VI:H/VA:H/SC:N/SI:N/SA:N = 5.9 MEDIUM. Local vector, low complexity but requires present timing (race condition), no privileges for the attacker role, no user interaction. Zero confidentiality impact. High integrity and availability impact on the vulnerable system only — no downstream/subsequent system impact. |
| Affected Versions | All CPython versions < 3.15.0: specifically 3.10.x, 3.11.x, 3.12.x, 3.13.x, 3.14.x. Python 2.x is EOL and unaffected by this specific fix. PyPy and other implementations may or may not share the vulnerable code path. |
| Fixed Versions | CPython 3.15.0 (main branch, expected GA October 2026). Backport PRs merged: 3.14, 3.13, 3.15 branch. Manual backports pending for 3.12, 3.11, 3.10. Distro packages (Debian, RHEL, Ubuntu) will lag — watch python3 package changelogs. Red Hat tracking: Bugzilla #2543492. |
| Scanning & Exposure | Not applicable for network scanning. This is a local vulnerability — Shodan, Censys, GreyNoise, and FOFA cannot detect it. Vulnerability scanners (Qualys, Tenable, Rapid7) will need local authenticated checks against installed Python versions. Expect plugin coverage within 2-4 weeks. |
| Disclosure Timeline | Reserved: 2026-06-15. Published: 2026-09-29. Fix PR: #157580 by StanFromIreland, reviewed by encukou. |
| Reporter & Credits | Reporter: Bitshift. Coordinator: Stan Ulbrych (StanFromIreland). Fix developer: Petr Viktorin (encukou). Disclosed responsibly through CPython's security process. |
Sources.
- CPython Issue #157579 — Race condition in tempfile.TemporaryDirectory
- CPython Fix PR #157580 — fd-based cleanup to prevent symlink following
- CVE-2026-12345 Detail — cve.threatint.com
- CVE-2026-12345 Exploits & Severity — Feedly
- CVE-2026-12345 Threat Intelligence — OffSeq Radar
- Strix.ai CVE-2026-12345 Analysis
- Python Version Support Schedule — devguide.python.org
- CVE-2023-6597 (predecessor) — cvedetails.com
Why this verdict
- Local-only attack vector eliminates remote mass exploitation. The attacker must already have shell access on the target host. This is a post-initial-access primitive, not an initial access vector. For a 10,000-host fleet, this means the attacker has already breached your perimeter and landed on a specific box before this CVE becomes relevant.
- Race condition adds real friction. The TOCTOU window is narrow and requires the cleanup to hit the error-handling code path (not the default happy path). There is no public exploit or weaponized tool, meaning each attacker must develop custom tooling. This is a deterministic friction multiplier that suppresses opportunistic exploitation.
- Impact ceiling is file deletion and permission clobbering, not code execution. Even a successful exploit cannot achieve RCE, privilege escalation to shell, or lateral movement directly. The attacker can destroy files or reset permissions — impactful for availability and integrity, but the blast radius is confined to the single host.
- No PoC, no KEV, no campaigns. One day post-disclosure with zero exploitation evidence, no public PoC, and no scanner coverage yet. The real-world threat pressure is near zero today.
- Role multiplier: CPython is deployed on virtually every host class — workstations, CI/CD runners, app servers, ML training nodes, and data tiers. However, the attack requires a *multi-user scenario* where an unprivileged attacker shares a host with a privileged Python process using
TemporaryDirectory. On CI/CD runners, this could occur on shared runners where build jobs run under different UIDs — a successful exploit could delete build artifacts or cached dependencies (host-level blast radius, not supply-chain). On production app servers, single-tenant containers or dedicated VMs eliminate the multi-user prerequisite entirely. On domain controllers and identity infrastructure, Python is rarely the primary runtime. The worst plausible high-value-role outcome is host-level data destruction on a shared CI runner — significant but not domain/fleet/supply-chain scale. This does NOT trigger the HIGH floor because the outcome in realistic high-value deployments stays at host level, and the multi-user prerequisite filters out the vast majority of production deployments.
Why not higher?
Upgrading to HIGH would require either a network-reachable attack vector or a code execution outcome, and this CVE has neither. The local-only, race-dependent, destruction-only impact profile is textbook MEDIUM. Even considering CPython's ubiquity, the multi-user prerequisite and the file-deletion-only impact cap the blast radius at host level in every deployment role analyzed. No exploitation evidence or PoC existence supports elevated urgency.
Why not lower?
Dropping to LOW would undercount the integrity and availability impact when the race IS won on a multi-user system running privileged Python. A successful exploit against a root-owned cleanup process can destroy arbitrary files — including OS configuration, database files, or application state. The predecessor CVE-2023-6597 was rated 7.8 HIGH under CVSS 3.1, and while CVSS 4.0 scoring is stricter, the underlying destructive capability is real. CPython's massive installed base means even a narrow attack scenario affects a non-trivial absolute number of hosts.
Crowdsourced verification payload.
Run directly on each target host using the system Python you want to check. Invoke as: python3 check_cve_2026_12345.py — no arguments, no privileges required. Exit code 0 means PATCHED, 1 means VULNERABLE, 2 means UNKNOWN.
#!/usr/bin/env python3
# CVE-2026-12345 checker - tempfile.TemporaryDirectory race condition
# Run on target host: python3 check_cve_2026_12345.py
# No special privileges required.
# Exit codes: 0=PATCHED, 1=VULNERABLE, 2=UNKNOWN
import sys
def main():
v = sys.version_info
impl = getattr(sys, 'implementation', None)
name = impl.name if impl else 'unknown'
print(f"[*] Interpreter: {name} {v.major}.{v.minor}.{v.micro}")
if name != 'cpython':
print("UNKNOWN - this check targets CPython only")
sys.exit(2)
# Python 3.15.0+ has the fix on the main branch
if (v.major, v.minor) >= (3, 15):
print("PATCHED - Python >= 3.15.0 includes the fix")
sys.exit(0)
# EOL versions will not receive backports
if (v.major, v.minor) < (3, 10):
print("VULNERABLE - EOL Python, no backport expected")
sys.exit(1)
# For 3.10-3.14, detect if the backported fix is present
# The fix introduces dir_fd-based operations in TemporaryDirectory._cleanup
try:
import tempfile
import inspect
src = inspect.getsource(tempfile.TemporaryDirectory._cleanup)
if 'dir_fd' in src:
print("PATCHED - backported fix detected in TemporaryDirectory._cleanup")
sys.exit(0)
except Exception as e:
print(f"[!] Could not inspect cleanup source: {e}")
print(f"VULNERABLE - Python {v.major}.{v.minor}.{v.micro} (no backport detected)")
sys.exit(1)
if __name__ == '__main__':
main()