KEVWire

Security intelligence, with context.

Response snapshot Oct 27, 2026, 01:11 UTC

Back to the wire
Permalink
VulnerabilityCVE-2026-113721

virtualenv: Generated files can follow existing destination links

Generated activation files and pyvenv.cfg can follow existing links and write outside the requested environment. The template writer tests exists() before unlinking, which misses dangling symlinks; the configuration writer opens the existing path directly.

At a glance

Exploited
Not known
Severity
4.5 Medium (CVSS 3.1)
Chance of exploitation
0.1% in the next 30 days
Sources
1 source

What to do

GitHub lists first patched versions: virtualenv: 21.14.9. Confirm the affected range and upgrade guidance in the exact advisory.

Read GitHub-reviewed advisory GHSA-558c-9hv2-4prj (opens a new tab)

What happened

## Summary

Generated activation files and pyvenv.cfg can follow existing links and write outside the requested environment. The template writer tests exists() before unlinking, which misses dangling symlinks; the configuration writer opens the existing path directly.

Anthropic's OSS Scanner reported this as `ANT-2026-XBEOCBS1` in `virtualenv-20261015-005`, received on 15 October 2026.

## Affected area

Continue reading

Final file entries written during environment creation and seeding.

## Attacker-controlled input

Another account must be able to prepare file links in the destination before creation. The invoking user must have write permission at the linked target. The destination parent must remain controlled during creation; concurrent parent-directory replacement is a separate concern.

## Affected code

The relevant 21.14.8 source is in [template writer](https://github.com/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/activation/via_template.py), [configuration writer](https://github.com/pypa/virtualenv/blob/a795ab116356afde11039cf2763d77e8a06da738/src/virtualenv/create/pyenv_cfg.py).

## Mechanism

`ViaTemplateActivator._generate()` removes an existing output only when `Path.exists()` is true. A dangling symlink therefore survives, and `write_bytes()` creates its target outside the environment. Other generated files have related final-entry behavior. `PyEnvCfg.write()` opens `pyvenv.cfg` without first replacing an existing link; a symlink or hard link there can overwrite an existing target.

These paths require a prepared destination tree and the invoking user's permission to write the linked target.

## Reproduction and evidence

The scanner's project tree contains `venv/bin/activate -> ../../../.bash_aliases` and an executable `venv/bin/basename`. Before creation, a new interactive bash does not create the marker. A plain `virtualenv venv` writes the activation script through the dangling link, creating `~/.bash_aliases` as a regular file. Debian's stock `.

bashrc` sources that file; the next interactive bash adds `venv/bin` to `PATH` and the planted `basename` writes the marker. The reproducer checks that the link remains, `~/.bash_aliases` appears, and the second shell executes the planted file. A separate manual check links `pyvenv.cfg` to `~/.bashrc` and observes the latter replaced with generated configuration text.

The link lets creation write outside the requested environment. The attacker chooses the target path but the generated bytes come from virtualenv. The executable in the example was already in the prepared tree; this finding is not arbitrary code injection into an otherwise trusted destination. The scanner traces the template behavior to #1454, first released in 20.0.0b1. [PR #3431](https://github.

com/pypa/virtualenv/pull/3431) replaces pre-existing output links and uses exclusive creation for generated text. It does not make a destination safe if another account can replace parent directories during creation.

## Attack sequence

```mermaid sequenceDiagram participant A as Prepared project tree participant V as virtualenv writer participant H as User home participant B as Next interactive bash A->>V: venv/bin/activate links to missing ~/.bash_aliases V->>V: Path.exists() follows link and returns false V->>H: write activation bytes through link H-->>B: ~/.bash_aliases now exists B->>H: .bashrc sources ~/.

bash_aliases B->>A: PATH reaches planted venv/bin/basename A-->>B: marker created Note over V,H: pyvenv.cfg can also overwrite an existing linked target ```

## Reproducer files

The following files are from the scanner report for `ANT-2026-XBEOCBS1`. 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 shutil import subprocess import sys import tempfile

# set up: a fresh home with Debian's stock ~/.bashrc, which sources ~/.bash_aliases when that file exists home = tempfile.mkdtemp(prefix="home-") shutil.copy("/etc/skel/.bashrc", f"{home}/.bashrc") bash_aliases = f"{home}/.bash_aliases" marker = f"{home}/attacker-ran" open_terminal = ["bash", "-i", "-c", "true"] terminal_env = {"HOME": home, "PATH": "/usr/bin:/bin", "TERM": "dumb"}

# the attacker's repository, cloned by the user to ~/proj checkout = f"{home}/proj" subprocess.run([sys.executable, "/tmp/repro.d/make_hostile_checkout.py", checkout], check=True) activate = f"{checkout}/venv/bin/activate" print("shipped: venv/bin/activate ->", os.readlink(activate), flush=True)

# a terminal opened before virtualenv runs does not touch the checkout (stderr: bash -i without a tty warns) subprocess.run(open_terminal, env=terminal_env, stderr=subprocess.DEVNULL, check=True) print("before virtualenv: ~/.bash_aliases exists:", os.path.lexists(bash_aliases), "| marker exists:", os.path.exists(marker), flush=True) assert not os.path.lexists(bash_aliases) and not os.path.exists(marker)

# trigger: the user creates an environment in the checkout, then opens a new terminal subprocess.run(["virtualenv", "--no-periodic-update", "venv"], cwd=checkout, env={**os.environ, "HOME": home}, check=True) subprocess.run(open_terminal, env=terminal_env, stderr=subprocess.DEVNULL, check=True)

# check: the activation script belongs in venv/bin/activate; it must not be written through the link to ~/.bash_aliases print("venv/bin/activate is still the symlink:", os.path.islink(activate)) written_outside = os.path.isfile(bash_aliases) and not os.path.islink(bash_aliases) print("~/.bash_aliases created as a regular file:", written_outside) if written_outside: print(" its first line:", open(bash_aliases).readline().rstrip()) attacker_code_ran = os.path.exists(m

Affected package ranges reported by GitHub: virtualenv: < 21.14.9

Scores and how they were measured

CVSS base severity

4.5

Version 3.1As of

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

Measurement source (opens a new tab)

How bad it is if exploited. Not a prediction of exploitation or proof you are exposed.

EPSS probability

0.1%

Scored Percentile 3.6%

Model and measurement source (opens a new tab)

A model's estimate of exploitation in the next 30 days, not certainty or your own risk.

Related technology

Sources

  1. Primary source

    GitHub-reviewed advisory GHSA-558c-9hv2-4prj (opens a new tab)

    The public advisory supplies the description, affected package/version ranges, and any listed CVSS metric. GitHub review is not local review.

    Published Checked

  2. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-558c-9hv2-4prj, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

  3. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-558c-9hv2-4prj, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

  4. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-558c-9hv2-4prj, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

  5. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-558c-9hv2-4prj, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

  6. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-558c-9hv2-4prj, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

  7. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-558c-9hv2-4prj, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

  8. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-558c-9hv2-4prj, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

  9. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-558c-9hv2-4prj, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

  10. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-558c-9hv2-4prj, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

  11. Primary source

    GitHub public advisory API page (opens a new tab)

    The API page supplied GHSA-558c-9hv2-4prj, its actual identifiers, source dates, and structured metrics.

    Published Date not suppliedChecked

  12. Primary source

    FIRST measured EPSS score (opens a new tab)

    Measured EPSS probability and percentile for CVE-2026-113721, scored 2026-10-26. This is a model estimate, not proof of exposure.

    Published Checked

Cite this record

For factual claims, cite the primary sources above; cite KEVWire for its compilation, dates, and classifications.

APA

KEVWire. (2026, October 22). virtualenv: Generated files can follow existing destination links (CVE-2026-113721). https://kevwire.com/record/CVE-2026-113721

MLA

"virtualenv: Generated files can follow existing destination links (CVE-2026-113721)." KEVWire, 22 Oct. 2026, kevwire.com/record/CVE-2026-113721.

Chicago

KEVWire. "virtualenv: Generated files can follow existing destination links (CVE-2026-113721)." Last modified October 22, 2026. https://kevwire.com/record/CVE-2026-113721.

Evidence summary (plain text)

virtualenv: Generated files can follow existing destination links (Oct 22, 2026).
Generated activation files and pyvenv.cfg can follow existing links and write outside the requested environment. The
template writer tests exists() before unlinking, which misses dangling symlinks; the configuration writer opens the
existing path directly.

Anthropic's OSS Scanner reported this as `ANT-2026-XBEOCBS1` in `virtualenv-20261015-005`, received on 15 October 2026.
CVSS 4.5 (version 3.1; evidence as of Oct 22, 2026; https://github.com/advisories/GHSA-558c-9hv2-4prj).
EPSS 0.1% for the following 30 days, scored Oct 26, 2026 (https://api.first.org/data/v1/epss?cve=CVE-2026-113721&date=2026-10-26).
Sources:
GitHub-reviewed advisory GHSA-558c-9hv2-4prj: https://github.com/advisories/GHSA-558c-9hv2-4prj
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc&after=Y3Vyc29yOnYyOpK0MjAyNi0xMC0yM1QyMDo1MTozMFrOAAf0Mg%3D%3D
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc&after=Y3Vyc29yOnYyOpK0MjAyNi0xMC0yM1QyMDo0OToxMlrOAAf0LQ%3D%3D
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc&after=Y3Vyc29yOnYyOpK0MjAyNi0xMC0yM1QyMDo0NzoxN1rOAAf0Kg%3D%3D
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc&after=Y3Vyc29yOnYyOpK0MjAyNi0xMC0yM1QyMDo0NTozNlrOAAf0Jw%3D%3D
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc&after=Y3Vyc29yOnYyOpK0MjAyNi0xMC0yM1QyMDo0NTowNVrOAAf0JQ%3D%3D
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc&after=Y3Vyc29yOnYyOpK0MjAyNi0xMC0yM1QyMDo0Mzo0NVrOAAf0Iw%3D%3D
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc&after=Y3Vyc29yOnYyOpK0MjAyNi0xMC0yM1QyMDo0MzozMVrOAAf0Ig%3D%3D
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc&after=Y3Vyc29yOnYyOpK0MjAyNi0xMC0yM1QxODozNTowMVrOAAZRcw%3D%3D
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc&after=Y3Vyc29yOnYyOpK0MjAyNi0xMC0yM1QxODozMDoyN1rOAAY_5Q%3D%3D
GitHub public advisory API page: https://api.github.com/advisories?type=reviewed&per_page=100&sort=updated&direction=desc&after=Y3Vyc29yOnYyOpK0MjAyNi0xMC0yM1QxODoyOTowNVrOAAfz8g%3D%3D
FIRST measured EPSS score: https://api.first.org/data/v1/epss?cve=CVE-2026-113721&date=2026-10-26
Record ID: CVE-2026-113721.
Record origin: Ingested record.
AI evidence: Not assessed.
Known exploitation: Not assessed.
Zero-day status: Not assessed. KEV membership alone does not establish zero-day status.
About this data

This record was ingested from a data source. Retrieved: Oct 26, 2026, 19:40 UTC. Ingestion is not an independent editorial investigation.

A CISA KEV listing documents known exploitation, not necessarily a zero-day; absence from the catalog does not prove a vulnerability has never been exploited.

Technology tags indicate possible relevance, not verified exposure. Confirm the affected product, version, and configuration against your own inventory; KEVWire does not scan your systems.

A personal reading checklist on this device, not verified remediation.

Record JSON

Your account

Email alerts

Email alerts are not available yet. To follow your stack today, use its RSS feed, which honors your filters, or connect an AI agent through the API.

Suggest an improvement

Ideas, missing sources, corrections, and bugs are all welcome. No account needed. Suggestions are reviewed by a person before anything changes.

Record details

Record detail