رأس القلم

CVE-2026-113721: virtualenv Link Writes and Fixes

CVE-2026-113721 is a local file-write problem in virtualenv. If another account prepares links in the destination, environment creation can write generated content outside the requested tree using the invoking user’s permissions. The maintainer advisory confirms version 21.14.8 and identifies 21.14.9 as the fix. It does not establish the first affected release.

The immediate decision is practical: check the virtualenv that creates environments, update through the appropriate package channel, and stop reusing destination trees that another account can populate. A new environment name is not sufficient if the parent directory remains outside your control.

The boundary is the output path, not Python package isolation

The advisory describes a template writer that removes an old entry only when Path.exists() returns true, and a configuration writer that opens an existing entry directly. A dangling symlink can therefore survive the first check. A symbolic or hard link at pyvenv.cfg can redirect the second write. The generated bytes remain constrained by virtualenv; this is not an arbitrary-content write primitive.

That distinction matters when interpreting alarming examples. Creating a file outside an environment is a violation of the intended filesystem boundary. It does not, by itself, show that an attacker can send a network request and execute arbitrary Python on every machine using virtualenv. Follow the prerequisites and the generated content, rather than treating the package name as proof of exposure.

A prepared dangling symlink redirects a generated file write outside the requested environment
Conceptual model: the target is outside the environment; generated bytes and account permissions constrain impact.
Explore authorized application testing with Penligent →

Why an absent target can leave a dangerous entry

There are two objects to think about: the directory entry and the object it names. A symlink can exist even when its target does not. The Python pathlib documentation explains that exists() normally follows symlinks, while is_symlink() can identify a broken link. A negative result from the former therefore cannot be treated as proof that the output name is unused.

A hard link presents a different trap. It is another name for the same file, so an ordinary-looking output path can share storage with a file elsewhere. Replacing the entry before generating new content separates the new output from that shared object. The right question is not simply whether a path string starts with the environment directory; it is which object the write actually opens.

For a review, sketch the operation in order: choose an output name, inspect any existing entry, remove or replace it, then open the destination. Mark every point at which the filesystem can resolve a link. This is more useful than searching only for the word “symlink,” because a writer may inherit the behavior from a high-level file API.

Check the creator and the destination separately

Version evidence and workspace ownership answer different questions. The GitHub-reviewed database entry uses the broader package range <21.14.9, while the maintainer entry records the confirmed 21.14.8 release and cautions against inferring a first affected version. Treat older releases as requiring assessment and update; do not present an untested earliest version as established fact.

الوضعEvidence to collectPractical response
Creator reports 21.14.8Executable, importing interpreter and installed package location.Use the fixed release through the supported installation channel.
An older creator is installedApplicable package advisory and any distributor backport.Do not assume safety from the narrower confirmed-release field.
A fresh destination under a controlled parentDirectory ownership and who can modify the parent.Keep that boundary intact during creation.
A reused or shared environment treeWho populated its entries and whether other jobs can alter them.Preserve relevant evidence, then create a separate trusted destination.
Distribution package rather than PyPI installationDistribution package revision and current security status.Follow the distribution’s fix information; do not compare only upstream-looking version text.

As checked on October 23, 2026, the Amazon Linux security page lists Medium severity, CVSS 3.1 score 4.5, and Pending Fix for the displayed Amazon Linux package rows. That is a distribution snapshot, not a statement that upstream lacks a fix. Recheck it before changing a managed image.

On a developer machine, identify whether your editor, shell or task runner invokes the same creator you inspected. In CI, include the runner image and wrapper command in the record. A package upgrade in an interactive terminal may not touch a tool installed in a separate automation environment. These are inventory recommendations, not a remote detection signature for this CVE.

# Read-only inventory; run with the interpreter used by your workflow.
python3 -m virtualenv --version
python3 -m pip show virtualenv

# A distribution-managed installation may require its package manager instead.
# Record the command, interpreter, package location and runner image together.

These inventory commands are provided for your environment and were not executed against a virtualenv installation for this article. A missing module or a missing pip command is an inventory finding, not a pass. Avoid upgrading a system-managed Python package with a different package manager merely to make a version number match.

A contained demonstration of the link semantics

The following toy program creates every file inside one temporary lab directory and removes that directory on exit. Its “outside” file is outside the example environment subdirectory, but still inside the disposable lab. It does not invoke virtualenv, alter a real shell startup file, launch a shell or reproduce the vendor’s full attack chain.

Save it as link_boundary_demo.py and run python3 link_boundary_demo.py on a local system that permits symbolic and hard links. The first case models an existence-guarded writer. The second replaces the final entry and uses exclusive creation. The third shows why checking only for symbolic links misses a hard-link alias.

from pathlib import Path
from tempfile import TemporaryDirectory
import os
import platform

# Local filesystem model only: no virtualenv package, shells or user files.
with TemporaryDirectory(prefix='link-boundary-demo-') as tmp:
    root = Path(tmp)
    env = root / 'environment'
    env.mkdir()
    target = root / 'outside-env.txt'  # Still inside this temporary lab.
    output = env / 'activation.txt'
    output.symlink_to(target)
    print('dangling exists:', output.exists())
    print('dangling is_symlink:', output.is_symlink())

    # Model of the unsafe existence-guarded replacement pattern.
    if output.exists():
        output.unlink()
    output.write_text('generated-demo-content\n')
    print('unsafe write reached lab target:', target.exists())

    # Model of final-entry replacement; not a general path-safety library.
    output.unlink()
    target.unlink()
    output.symlink_to(target)
    output.unlink()
    with output.open('x') as stream:
        stream.write('generated-demo-content\n')
    print('replacement is regular file:', output.is_file() and not output.is_symlink())
    print('lab target remains absent:', not target.exists())

    original = root / 'hardlink-target.txt'
    original.write_text('original\n')
    alias = env / 'config.txt'
    os.link(original, alias)
    alias.write_text('changed-through-alias\n')
    print('hard link shares written bytes:', original.read_text() == 'changed-through-alias\n')
print('runtime:', platform.python_version(), platform.system())

This exact program ran locally on October 23, 2026, using Python 3.12.14 on Linux. Its observed output was:

dangling exists: False
dangling is_symlink: True
unsafe write reached lab target: True
replacement is regular file: True
lab target remains absent: True
hard link shares written bytes: True
runtime: 3.12.14 Linux

The useful result is the mismatch between exists و is_symlink, followed by the different write locations. This demonstrates filesystem behavior under a controlled setup. It does not establish that an installed virtualenv release is affected or fixed. Permission errors on another platform also would not establish safety; they would mean that this demonstration’s prerequisites were not met.

What the patch changes, and what still needs protection

في virtualenv PR #3431, the maintainer explains that generated outputs are replaced with fresh file objects and generated text uses exclusive creation. The change also addresses linked entries encountered before delegation to the standard-library creator. This changes the write operation itself rather than relying on a prior target-existence check.

إن 21.14.9 release notes explicitly include replacement of symbolic and hard links for configuration, activation scripts, import hooks and seed launchers. They also describe replacement of executable-directory links and Windows junctions before interpreter-file creation. Use the released correction rather than copying the toy writer into production tooling.

The maintainer’s remaining condition is important: the destination must be protected from concurrent parent-directory replacement. Exclusive creation at the final filename is not a general defense for an attacker-controlled path hierarchy. The safest operational choice is a fresh destination beneath a parent owned and controlled by the creating account.

Do not turn this into a blanket instruction to delete every symlink in a development tree. Links can be intentional, and a recursive cleanup can damage unrelated work. Keep replacement scoped to the environment being recreated, preserve suspicious entries before investigation, and let the corrected tool manage its own outputs.

Corrected virtualenv writer and controlled destination protect separate filesystem boundaries
Suggested remediation workflow; a corrected writer does not make an attacker-controlled parent directory safe.
Evaluate your security testing workflow →

Close the remediation with a clean creation path

A sensible acceptance test has two parts. First, retain evidence of the corrected creator actually used by the workflow. Second, run an approved environment-creation smoke test in a newly allocated, controlled destination and confirm the application can use the result. Do not reuse a questionable tree just because a package update succeeded.

تحققUseful evidenceLimit
Creator correctedVersion, package source and invoking interpreter.Does not repair files already written outside a previous environment.
Workspace controlledFresh destination and documented parent ownership.A pathname alone says nothing about concurrent writers.
Functional creation succeedsRecorded creation command and expected application smoke-test result.A normal run is not a negative exploit test.
Historical exposure reviewedRelevant job history, file metadata and preserved suspicious entries.Missing logs do not prove no earlier modification occurred.

If a previously shared tree is in scope, review what the creating account could write and preserve relevant records before rebuilding. An unexpected generated configuration file deserves investigation, but the mere presence of a link is not proof of compromise. Separate the confirmed package weakness, local exposure conditions and any evidence of actual misuse.

For a broader authorized application assessment, evaluate whether Penligent’s workflow meets your requirements for test scope and reviewable evidence. That is a separate product evaluation: this local filesystem example does not demonstrate a Penligent virtualenv detector or establish coverage of this CVE.

The video below introduces AI-assisted pentesting. It is related background for application testing, not a reproduction of CVE-2026-113721 or a virtualenv patch-verification procedure.

Questions that change the response

Is activating an existing environment the trigger?

The documented weakness concerns file generation during environment creation and seeding. Do not confuse that operation with merely using an already-created interpreter. Investigate the actual commands run in the affected workflow.

Does this require administrator privileges?

The local write uses the invoking account’s permissions. Running creation with broader privileges can increase what is writable; that is a reason to minimize privileges, not a claim that ordinary use requires root.

Does upgrading recover overwritten files?

No. Treat installing the corrected creator and reviewing earlier filesystem changes as separate tasks. Restore any affected files from trusted sources only after preserving evidence appropriate to the investigation.

Start with the creator binary and the destination owner. Updating virtualenv addresses the documented write behavior; creating environments in a controlled location keeps the rest of the path from undermining that correction.

شارك المنشور:
منشورات ذات صلة
arArabic