GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||
GeezerVison is showing github.com — 🐛 fix(seed): copy from temporary app-data by gaborbernat · Pull Request #3437 · pypa/virtualenv Conversation
|
|
Thank you for the fix. A request on the advisory record: GHSA-4f8h-vr72-p32c (CVE-2026-113713) lists the affected range as As it stands, once the advisory is reviewed into the global advisory database, OSV and pip-audit will flag every release from 21.14.8 on, 21.14.10 and later included, with no fixed version to move to. Downstream we are setting a |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
hanks for flagging it. GHSA-4f8h-vr72-p32c now records 21.14.10 as the patched version. The affected range is >= 20.0.6, < 21.14.10: the advisory traces the incomplete temporary-cache guard to #1661, first released in 20.0.6, so the earlier lower bound of 21.14.8 understated it. Your >=21.14.10 floor matches the fix. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
…mporary app-data The advisory's range is >=21.14.8, and review expected it not to apply here. It does apply. When the app-data folder is unusable, virtualenv falls back to a temporary app-data folder and removes it after creation. The --symlink-app-data parser guard turns the command-line flag into a no-op for temporary app-data. But the same option set through VIRTUALENV_SYMLINK_APP_DATA=true or `symlink_app_data = true` in virtualenv.ini still reached FromAppData unchanged. Reproduced on this branch on py2.7 and py3.10, with VIRTUALENV_OVERRIDE_APP_DATA pointing below a regular file: - site-packages/pip was a symlink into the deleted temporary folder, and `bin/pip` failed with ImportError; - any user who recreated that path would have supplied the code the environment imports. The command-line form already copied. Port upstream b81995c (pypa#3437, 21.14.10). FromAppData uses symlinks only when the app-data is not transient, and logs that it copies instead. Test: test_transient_seed_survives_cache_cleanup covers the environment, configuration and command-line forms. pip must be a real directory and `python -m pip --version` must work. The environment and configuration cases fail without this change on py2.7 and py3.10. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…sories (#3) * Fix CVE-2026-113726, CVE-2026-113725, CVE-2026-113723: literal bash/zsh activate prompt GHSA-5vjq-rrrf-7h2q / GHSA-vxj4-q4g9-crj7 / GHSA-6w3r-xr62-4xcp. bin/activate (sourced by bash and zsh) put the prompt text - the --prompt value or the environment directory name - straight into PS1. Shells re-expand PS1 at every prompt draw: bash with promptvars (the default), zsh with PROMPT_SUBST. A name such as x$(touch PWNED)y, x`cmd`y, or one with a backslash before $ (which survives bash's prompt-escape decode as a live $), ran a command each time the prompt was shown. Older BASH_COMPAT modes and promptvars/posix combinations change which escaping is right. The three advisories are successive upstream fixes to the same few lines, so they are backported together as the final upstream (21.14.9) shape: - the prompt text goes into VIRTUAL_ENV_PROMPT (exported, unset on deactivate); - under bash with promptvars on or in posix mode, PS1 holds the literal reference '(${VIRTUAL_ENV_PROMPT-}) ', so the value is never itself re-expanded; - otherwise backslashes are doubled for the prompt-escape decode, and under zsh $, ` and % are also escaped (PROMPT_SUBST can be set after activation); - the PS1 change is guarded to bash/zsh so POSIX shells (dash) still source the script. This also repairs the PS1 lines of the earlier CVE-2024-53899 backport (7e69a41), which read `PS1=(__VIRTUAL_PROMPT__) ${PS1-}`: bash parsed that as an array-assignment prefix to a command, so in an interactive shell the venv prompt was never set and the user's old PS1 was executed as a command ("\u@\h: command not found"). That is why test_bash failed on the base. Upstream: 5b91536 (GHSA-5vjq, 21.14.4), c3104e0 (pypa#3390, 21.14.7), 5f48d71 (pypa#3418, 21.14.9). Python 2.7: tests use Popen/communicate and distutils find_executable instead of subprocess.run/shutil.which. test_bash now renders PS1 with ${PS1@P} rather than reading the exported variable, as upstream does. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Fix CVE-2026-113732 / GHSA-c947-3pg5-gm8q: fish quoting of backslash before quote FishActivator inherited shlex.quote, whose POSIX rules leave a backslash literal inside single quotes. fish reads \' (and \\) as escapes inside single quotes, so a venv path or --prompt with a backslash before a quote, e.g. x\'(touch PWNED)'\', ended the literal early and ran the rest as commands when activate.fish was sourced or the prompt drawn. Give FishActivator its own quote() that escapes backslashes and single quotes. Upstream (3bddeaf, 21.14.2; refined in 685435d/pypa#3413) first escaped them inside one quoted string, then moved each escape outside the quotes as 'a'\\'b' so a legacy (non-UTF-8) decoder cannot absorb the backslash into a preceding multibyte character. That final form is NOT safe on fish 3.0.x (Debian buster, the era of this Python 2 line): fish 3.0.2 misparses an escaped backslash between two quoted segments and the same payload still executes. This backport puts each escape in a quoted segment of its own ('a''\\''b'), which keeps the escape next to an ASCII quote and parses correctly on fish 3.0.2 and 4.0.2. GHSA-p58f-9548-mpm2 (paths in bash/fish activation) is covered by the same regression test: the two upstream sites it fixed do not exist in 20.15.1 (see the release notes), and the remaining fish path placeholders are now quoted with this function. Test: test_fish_activate_does_not_run_commands drives fish with prompt and directory-name payloads; the backslash-quote cases fail without this change on both fish 3.0.2 (py2.7 image) and fish 4.0.2 (py3.10 image). Python 2.7: str.format, Popen/communicate, distutils find_executable; the script path is passed through the environment because fish < 3.1 sets no $argv for -c. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Fix CVE-2026-102938 / GHSA-9h9j-4vrj-gf7g: keep pyvenv.cfg values on one line The activators write the --prompt value into pyvenv.cfg (prompt = ...). pyvenv.cfg is a line-based key = value format with no escaping, so a prompt carrying a newline (or any other str.splitlines() boundary, e.g. \v, \x1c, \x85, U+2028) was read back as extra configuration lines, letting it set keys such as include-system-site-packages or home that CPython's site.py and virtualenv itself honour. PyEnvCfg.write() now replaces every line boundary in keys and values with a space via a new virtualenv.util.text.collapse_line_boundaries helper (the same helper upstream later shares with the batch activator). Upstream: a30f995 (pypa#3247, 21.7.11) plus the helper extraction in d721ff1 (pypa#3250). Python 2.7: \u escapes under unicode_literals instead of chr(), values coerced through "{}".format() as write() already did, no Final annotations. Upstream's hypothesis property test is replaced by a parametrized unit test over every boundary plus an end-to-end --prompt injection test; both fail without the change. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Fix CVE-2026-102937 / GHSA-x78j-v8h9-3j2q: escape batch activator values BatchActivator.quote() returned its input unchanged, so a --prompt such as x" & calc & " closed the quoted set "PROMPT=(...)" string in activate.bat and ran the rest as cmd.exe commands when the script was called; a line break in the value started a new statement outright. quote() now doubles % (cmd's in-file escape), replaces the operators cmd honours even inside quotes (& | < > ^ ! and ") with a space, and collapses line boundaries. An & cannot be neutered in the destination path without pointing activate.bat at a different directory, so generate() refuses to write the batch scripts (with a warning) for a destination containing &, as upstream does. Upstream: d721ff1 (pypa#3250, 21.7.12). Python 2.7: str.format instead of f-strings, logging module instead of a module LOGGER (matches this tree), no Final annotations; only __VIRTUAL_ENV__ is checked because 20.15.1 has no Tcl/Tk replacements. test_batch.py used an f-string (from the earlier CVE-2024-53899 backport) that made it a SyntaxError on Python 2; replaced with str.format. The new quote/injection/skip tests run on any OS and fail without this change. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Complete the CVE-2024-53899 backport in the csh, PowerShell and Nushell templates The earlier hand cherry-pick of upstream 86ddded (CVE-2024-53899 / GHSA-rqc4-2hc7-8c8v) made every activator quote its own placeholder values, but the 20.15.1 templates differ from the ones that commit targeted and a few placeholders were left in places where the added quoting is wrong: - activate.ps1 kept the prompt inside an expandable string, ("(__VIRTUAL_PROMPT__) " + ...), so the single-quoted value landed inside double quotes and a --prompt such as x$(New-Item PWNED)y ran its subexpression at every prompt draw. It is now concatenated as a literal: ("(" + __VIRTUAL_PROMPT__ + ") " + ...). - PowerShellActivator.quote() only doubled the ASCII apostrophe; PowerShell also accepts U+2018-U+201B as single-quote delimiters, so a curly quote ended the literal early (upstream 876c5ba / pypa#3325). All five are doubled now. - activate.csh read `set env_name = (__VIRTUAL_PROMPT__) `, which makes a word list instead of the "(prompt) " prefix, so --prompt was never shown (test_csh[with_prompt] failed on the base). Restored as '('__VIRTUAL_PROMPT__') '. - activate.nu wrapped the already-quoted __PATH_SEP__ and __DEACTIVATE_PATH__ values in a second pair of single quotes, producing '<raw string>' text; the extra quotes are removed (not exercised by the tests here: no nushell in the test images). Tests: test_powershell gains a quote() check for every delimiter, a generated-text check, and an end-to-end check that sources activate.ps1 in pwsh 7.4.6 and renders the prompt; all fail before this change (the pwsh one creates PWNED) and pass after. test_powershell.py used an f-string that made it a SyntaxError on Python 2; replaced with str.format. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Fix CVE-2026-113720 / GHSA-fc2g-4w5p-6whm: apply the current umask to copied files virtualenv copies interpreter files (builtin creator with --copies) and seed package files (app-data "copy" installer) with shutil.copy, which also copies the source's permission bits. A cached file whose mode was set under an older, more permissive umask (e.g. group/world writable in a shared app data folder) therefore propagated that mode into every new environment, regardless of the umask in effect when the environment was created. The fix copies file contents into a destination created with os.open(O_WRONLY | O_CREAT | O_EXCL, 0o755 or 0o644), choosing 0o755 only when the source has any executable bit. The kernel applies the current umask at creation, without touching the process-wide umask, and source modes are no longer carried over. copytree now goes through the same helper for every file. Upstream: pypa/virtualenv 35f0b33 (PR pypa#3425), released in 21.14.9. Python 2.7 adaptations: io.open on the raw fd instead of os.fdopen with Path.open; O_BINARY taken via getattr(os, "O_BINARY", 0); makedirs with errno.EEXIST handling instead of mkdir(exist_ok=True); paths go through the existing norm() helper so CPython 2 on Windows keeps unicode paths. Tests (test_copy_uses_current_umask, test_copy_replaces_contents) adapted from upstream without type annotations/Final. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Fix CVE-2026-113721 / GHSA-558c-9hv2-4prj: replace links instead of writing through them virtualenv wrote its generated files (pyvenv.cfg, activation scripts, .gitignore, _virtualenv.pth/_virtualenv.py, seed console scripts, and on Python 2 the site.py shim) with plain open-for-write calls that follow an existing symbolic link at the destination, and write into the shared inode of a hard link. Someone able to plant such a link in a destination that is later (re)used for an environment could make virtualenv overwrite or create an arbitrary file the invoking user can write. The stdlib venv creator has the same issue for the interpreter files, pyvenv.cfg and activation scripts it writes into the executable directory, which itself could be a planted directory link. Fix (as upstream): - util.path.write_file(): unlink any existing entry (file, symlink, hard link), then create the file exclusively (O_CREAT | O_EXCL), so a link re-planted between the unlink and the open makes the write fail. - Use write_file for pyvenv.cfg, activation scripts (via_template, only the write call), .gitignore, _virtualenv.pth / _virtualenv.py. - ScriptMakerCustom._write_script: remove symlinks at the script names before distlib opens them (distlib only removes paths that exist(), so a dangling link was followed). Minimal, self-contained hunk. - venv creator: ensure_within(dest, bin_dir) makes every component of the executable directory a real directory (replacing directory symlinks and Windows junctions, rejecting non-directories), then removes symlinks and multiply-linked files at pyvenv.cfg, .gitignore and inside bin_dir before calling stdlib venv. Upstream: pypa/virtualenv d25f286 (PR pypa#3431), released in 21.14.9. Fork-specific / Python 2.7 adaptations: - write_file opens via os.open(..., 0o666) + io.open(fd, "wb") instead of Path.open("xb") (no "x" mode on Py2); O_BINARY via getattr. - ensure_within uses os.lstat/os.mkdir with errno checks (no FileNotFoundError / FileExistsError / exist_ok on Py2), raises OSError(errno.ENOTDIR, ...) which Py3 maps to NotADirectoryError; junction detection uses st_reparse_tag when present (Py3.8+ on Windows) - Py2 on Windows cannot detect junctions. - No `*` unpacking in tuple literals: itertools.chain in venv.create. - 20.15.1 has no CACHEDIR.TAG, so that upstream hunk does not apply. - Additionally routes the Python 2 creator's site.py shim through write_file (Py2-only generated file upstream no longer has). Tests (adapted from upstream): test_creator_replaces_generated_file_links (adds site.py; seeds only for the pip case), test_venv_prepares_real_ executable_directory, test_venv_recreate_with_copied_interpreter, test_venv_replaces_hard_linked_files, test_write_file_replaces_content, plus test_write_file_replaces_link. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Fix CVE-2026-113716 / GHSA-cw65-gx24-rc82: replace linked environment directories The builtin creator prepared its directories (bin, lib/pythonX.Y, site-packages, ...) with ensure_dir(), which only checks exists() and so accepts a pre-existing symlink (or Windows junction) at any component. The interpreter files, pyvenv.cfg-adjacent payload and later seed packages were then written through that link into a directory outside the environment. The stdlib venv creator likewise created/used the library directories without checking them. Fix (as upstream): the builtin creator creates every directory with ensure_within(dest, directory), which walks each component below dest, replaces a directory symlink or junction with a real directory (logging a warning), and rejects non-directories. The venv creator runs ensure_within for bin_dir, purelib and platlib before invoking stdlib venv and drops the post-creation ensure_dir() of the libs. Upstream: pypa/virtualenv 554d9f9 (PR pypa#3429), released in 21.14.9. ensure_within itself came in with the previous commit (upstream d25f286). Python 2.7 adaptations: tests use subprocess.check_call (no capture_output), str.format, no annotations; ensure_within raises OSError(ENOTDIR) (Py3 maps it to NotADirectoryError) so the regular-file test matches OSError; the junction test is skipped on Windows Python < 3.8 because st_reparse_tag is unavailable there. Tests: test_creator_replaces_directory_links, test_environment_directories_ {create_nested_path,reject_regular_file,reject_parent_components, remove_links,report_replaced_junction}. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Fix CVE-2026-113715 / GHSA-qmx5-4p3x-wvqf: stop reading back stdlib venv's pyvenv.cfg With the stdlib venv creator, virtualenv let stdlib venv write pyvenv.cfg, then re-read it (PyEnvCfg.refresh) and merged those values over its own ("prefer venv options over ours"). Stdlib venv writes values without escaping line boundaries, and from Python 3.11 it records its command line, including the destination, as `command = ... -m venv <dest>`. A destination containing a line boundary therefore became extra key = value lines that the readback reinterpreted as configuration. Reproduced on Python 3.12 with the unpatched fork: `--creator venv` without --system-site-packages and a destination ending in "\ninclude-system-site-packages = true" produced an environment whose pyvenv.cfg enables global site-packages. Fix (as upstream): - Venv.validate_dest rejects destinations containing any character str.splitlines() treats as a boundary (the util.text collapse_line_boundaries helper added for GHSA-9h9j-4vrj-gf7g), before and after the generic validation. Other creators keep their existing destination handling. - Venv.set_pyenv_cfg no longer refreshes/merges stdlib's intermediate configuration; it writes the configuration from the selected interpreter and virtualenv's options. Upstream: pypa/virtualenv b0008a5 (PR pypa#3426), released in 21.14.9. Deviation for the fork: upstream's base Creator.set_pyenv_cfg already writes home (directory of the base executable), version and executable, so dropping the merge loses nothing there. In 20.15.1 the base class writes home = sys.exec_prefix and no version/executable, so dropping the merge alone would change venv-created pyvenv.cfg files. Venv.set_pyenv_cfg now derives the keys stdlib venv contributed from the interpreter instead: home = dirname(abspath(system_executable)), version = X.Y.Z and, for 3.11+, executable = realpath(system_executable). The `command` key stdlib 3.11+ writes is not reproduced (it is what embeds the destination). GraalPy launcher keys are not applicable to 20.15.1. Python 2.7 adaptations: the check runs on ensure_text(value) since argv is bytes on Py2 (the venv creator can be selected from a Py2 host for a Py3 target), no annotations / f-strings. Tests: test_venv_creator_rejects_line_boundary (10 boundaries), test_venv_creator_accepts_plain_destination, test_venv_configuration_uses_selected_options (adapted: no GraalPy keys), and fork-specific test_venv_configuration_keeps_stdlib_keys. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Keep the new bash/fish activation tests independent of test order tests/unit/create/via_global_ref/builtin/testing/path.py sets "\" as the altsep of the shared POSIX path flavour when it is imported (pre-existing test helper, to fake Windows paths). In a session that imports it before the activation tests run, Path objects split directory names such as x\'(touch PWNED)'\' at the backslash, so the fish dirname cases created a nested directory instead of the payload name and failed. Build those destinations as plain strings and create the environments with `python -m virtualenv` in a fresh interpreter, so the regression tests exercise the intended names whatever ran first. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Fix CVE-2026-102930 / GHSA-94p9-xgh2-xp45: verify downloaded seed wheel digests When virtualenv downloads a seed wheel (--download, the background periodic update, or `virtualenv --upgrade-embed-wheels`), it shells out to `pip download` and then caches and seeds whatever file pip handed back. Nothing checked those bytes, so a compromised or stale index/mirror could get an arbitrary wheel cached in the app-data wheelhouse and installed into every environment created afterwards. download_wheel() now looks up the sha256 PyPI's JSON API publishes for the exact downloaded filename (https://pypi.org/pypi/<dist>/json, releases[<version>][].digests.sha256, through the existing cached _pypi_get_distribution_info helper) and raises if the file does not match. The check runs only when no custom index is configured through PIP_INDEX_URL / PIP_EXTRA_INDEX_URL / PIP_INDEX, since a private index can legitimately serve a rebuilt wheel under the same filename. download_wheel() also raises a clear RuntimeError instead of crashing with AttributeError when no downloaded wheel can be identified. Upstream: pypa/virtualenv a01ed3e (PR pypa#3251). The upstream follow-up 91460ad (PR pypa#3302, "fail closed when PyPI digest is unknown") is folded in here: a01ed3e alone let the wheel through when the PyPI lookup failed, so anyone able to block pypi.org/pypi could switch the check off. The lookup is retried three times (1s/2s/4s) and then raises UnverifiedWheelError; get_wheel() falls back to the bundled wheel for the default "bundle" version and re-raises for an explicitly pinned one. UnverifiedWheelError is also what the follow-up fix for GHSA-494p-63j8-28p3 builds on. Python 2.7 adaptations: no f-strings/walrus/Final annotations, str.format messages, six.string_types for the digest type check, module logging calls as in the rest of the file, str() env keys for the Py2 os.environ, contextmanager-based urlopen mocks in the tests (Py2 StringIO is not a context manager), and the retry sleep is mocked. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Fix CVE-2026-113714 / GHSA-494p-63j8-28p3 and CVE-2026-113717 / GHSA-cx28-75q4-x26q: verify wheels before caching GHSA-494p-63j8-28p3: download_wheel() ran `pip download -d <wheelhouse>` straight into the shared app-data wheel cache and then picked the result from pip's "Saved ..." / "File was already downloaded ..." output, falling back to the newest matching file already in the wheelhouse. The selection was therefore driven by pip's output and by whatever was already cached, and the file sat in the reusable cache before any verification ran, so a rejected or unverifiable wheel stayed there for later runs to pick up. GHSA-cx28-75q4-x26q: the pip seeder (--seeder pip --download) never downloaded through virtualenv at all; it dropped --no-index and let `pip install` resolve the pinned versions against the index and every --find-links folder, so no digest verification happened. Fix (upstream pypa/virtualenv 7babec6, PR pypa#3430): - download_wheel() downloads into a fresh private staging directory inside to_folder, requires exactly one regular (non-symlink) file in it, checks that its name is the requested distribution (PEP 503 normalized) and version, verifies its digest against PyPI, checks it supports the target Python, and only then moves it into to_folder. The staging directory is always removed; nothing reaches the cache on failure. pip's output is no longer parsed. - PipInvoke always passes --no-index, obtains every wheel through get_wheel(download=self.download) (so downloads go through the verified path above), and points pip at a temporary --find-links folder holding copies of only the selected wheels. Python 2.7 adaptations: tempfile.mkdtemp + safe_delete instead of TemporaryDirectory; os.makedirs with OSError tolerance instead of mkdir(exist_ok=True); os.replace when present, else os.rename (unlinking first on Windows); packaging is not a runtime dependency here, so name canonicalization is a local PEP 503 regex and the version check handles the two specifier forms this code generates ("==X", where trailing ".0" components are ignored for plain release versions, and "<X", compared on version tuples like find_compatible_in_house) instead of packaging.Specifier. Tests use contextmanager urlopen mocks. Test changes: test_base_bootstrap_via_pip_invoke now asserts --no-index and a single private find-links folder with exactly the selected wheels, and uses "embed" for setuptools since "bundle" with --download now downloads through virtualenv (as the app-data seeder already did). Co-Authored-By: Claude Opus 5.5 <[email protected]> * Fix CVE-2026-113722 / GHSA-c978-59m8-c77v: quote interpreter paths in POSIX console launchers When the interpreter path contains a space or is longer than the kernel shebang limit, distlib (used by the app-data seeder's ScriptMakerCustom and by pip itself in the pip seeder) writes a /bin/sh polyglot launcher: #!/bin/sh '''exec' "<dest>/bin/python" "$0" "$@" ' ''' The path is only double quoted, so the shell still expands $(...), backticks and $VAR inside the destination path: running bin/pip of an environment created at e.g. "/tmp/x $(touch PWNED)/env" executes the embedded command (reproduced on 20.15.1 with both seeders, py2.7 and py3.10). distlib also only checks for a space, so a tab in the path yields a plain shebang the kernel splits. Fix (upstream pypa/virtualenv 2519941, PR pypa#3417): - base.py: new shell_shebang() builds the launcher with the path single quoted via shlex quoting, and backslashes rewritten as '"\\"' so both sh and Python (which reads the line as a string literal) see the original path. ScriptMakerCustom._build_shebang() uses it on POSIX whenever distlib's shebang contains any whitespace. - pip_invoke.py: after the pip seeder runs on POSIX, launchers of the seeded distributions listed in their RECORD (inside the script dir) that use the sh form, or a plain shebang with whitespace in the interpreter path, are rewritten with shell_shebang() (unlink + O_EXCL create, keeping the mode) and their RECORD hash/size refreshed. Python 2.7 adaptations: six.moves.shlex_quote instead of shlex.quote; importlib.metadata is unavailable, so the seeded *.dist-info/RECORD files are found by listing purelib/platlib and parsed with csv using bytes I/O on Python 2 and text I/O (newline="") on Python 3; paths go through ensure_text before encoding. Tests: test_console_script_preserves_interpreter_path (both seeders, running pip through its launcher and through bin/python, with plain, space, apostrophe, command-substitution, long, backslash and tab destinations; the command-substitution case asserts nothing was executed) and test_pip_launcher_record_matches_contents. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Fix CVE-2026-113719 / GHSA-hhxq-47xx-j5xx: contain seed metadata cleanup Before the app-data seeder installs pip/setuptools/wheel into an environment that already has them, PipInstall._uninstall_dist() removes the previous version using the installed distribution's own metadata: every top_level.txt line and every RECORD path, joined onto site-packages, was deleted (recursively for directories) with no containment check. Planted or corrupted metadata could therefore make a re-seed (e.g. `virtualenv <existing env>`) delete arbitrary paths: an absolute top_level.txt line naming the environment or any directory, a blank line naming site-packages itself, or a RECORD entry such as ../../../../keep.txt reaching outside the environment. Fix (upstream pypa/virtualenv e3cea6f, PR pypa#3428): resolve each entry and remove it only if it lies inside the resolved environment directory and is not the metadata folder's own ancestor (site-packages, lib, the env root); otherwise log "skip removal of ..." and keep it. A symlink entry is judged by its resolved parent plus its own name, and is unlinked without following it, so launcher and --symlink-app-data entries keep working. Python 2.7 adaptations: pathlib2 Path API (resolve(), parents membership) via virtualenv.util.path, logging calls and read_text() without encoding as in the existing code, no Final annotations. Tests: test_seed_metadata_cleanup_stays_inside_package (top-level-env-root, top-level-blank, record-traversal), test_seed_metadata_unlinks_symlink_without_following, test_seed_metadata_keeps_reseeding_supported (copy, symlink). Co-Authored-By: Claude Opus 5.5 <[email protected]> * Harden the PyPI digest lookup behind CVE-2026-102930 / GHSA-94p9-xgh2-xp45 The downloaded-seed-wheel verification takes its expected sha256 from PyPI's JSON API (_pypi_get_distribution_info). In 20.15.1 that lookup had two problems that matter now that it is a security check: - It silently retried over unverified HTTPS (ssl._create_unverified_context()) when the verified request failed, so an on-path attacker who can break TLS verification could also supply the "expected" digest. As upstream does (21.x), the unverified retry is now only used when VIRTUALENV_PERIODIC_UPDATE_INSECURE is set, with a warning. - It used `with urlopen(...)`, but on Python 2 the urlopen result is not a context manager (AttributeError: __exit__), so every lookup failed on Python 2. With the fail-closed verification that would make every download on Python 2 fall back to the bundled wheel (or fail for a pinned version). Responses without __enter__ are now read under contextlib.closing. Tests: default no unverified retry, the opt-in retry, a Python-2-style response object, and the three periodic-update tests now expect a single request (as upstream's do). The new tests fail without this change. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Keep the console-launcher regression test independent of test order test_console_script_preserves_interpreter_path[*-backslash] created its environment in-process; after tests/unit/create/.../testing/path.py has been imported (it sets "\" as the altsep of the shared POSIX path flavour) the backslash in the destination became a separator and the case failed in a full `pytest tests/unit` run. Create the environment with `python -m virtualenv` in a fresh interpreter instead. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Pass --no-deps to the pip seeder's offline install The CVE-2026-113717 / GHSA-cx28-75q4-x26q backport makes the pip seeder install only the selected, verified wheels with --no-index. With --download on Python 3.10 the newest wheel (0.48) depends on packaging, which is not among them, so `virtualenv --seeder pip --download` failed ("No matching distribution found for packaging>=24.0"); before the backport pip quietly fetched that dependency, unverified, from the index. Install with --no-deps, which is what the app-data seeder has always done (it unpacks the seed wheels without dependencies), so both seeders seed the same set and nothing outside the verified wheels is consulted. Verified end to end in the py3.10 image: `--seeder pip --download` now creates the environment with pip 26.3, setuptools 84.2.1, wheel 0.48.0. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Release 20.15.1.post3: security backports for 18 virtualenv advisories ActiveState security release on the 20.15.1.x (Python 2.7) line. - docs/changelog.rst: v20.15.1.post3 section with the CVE / Advisory / Status table for all 18 advisories published since 20.15.1+security.2 (15 patched, 3 not affected with the reason) and the related bug fixes. - setup.py: default SETUPTOOLS_SCM_PRETEND_VERSION to 20.15.1.post3. The version used to come from the nearest git tag via setuptools_scm, so an untagged checkout of this branch built as 20.15.2.devN+g<sha>; pinning it makes every build of the line (sdist, wheel, CI) report 20.15.1.post3, and an explicit SETUPTOOLS_SCM_PRETEND_VERSION still wins. - version.py (repo root, read by towncrier only): 20.15.1.post3. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Fix CVE-2026-113776 / GHSA-8rjx-v5ww-45pp: substitute activation placeholders in one pass ViaTemplateActivator.instantiate_template replaced the placeholders one key at a time over the whole template. When a quoted value contained the name of a placeholder handled later (e.g. a parent directory named A__VIRTUAL_NAME__B), the later pass rewrote that name inside the finished literal, and the inserted value's own quotes closed it. With a destination such as /tmp/t/A__VIRTUAL_NAME__B/x'$(touch PWNED)'y, bash wrote VIRTUAL_ENV='/tmp/t/A'x'"'"'$(touch PWNED)'"'"'y'B/x'"'"'$(touch PWNED)'"'"'y' and `. bin/activate` (and `source bin/activate.fish`, and csh) ran the command. The per-key loop and the quoting came in with the CVE-2024-53899 backport, so 20.15.1+security.2 is affected too, although the advisory's range (>=20.26.6) keeps it out of an OSV query for 20.15.1. Build every placeholder's final quoted text first (new substitutions() hook) and replace them all in one regex pass, longest key first, so text inside a substituted value is never scanned again. 20.15.1's csh activator has no display tokens, so upstream's csh part has nothing to port. Upstream: 602b266 (21.14.2). Python 2.7: str.format-free, dict comprehension, an empty mapping returns the template unchanged. Tests: - test_placeholder_values.py: every activator (bash, batch, csh, fish, nushell, PowerShell, python) rendered with a destination and prompt that embed each placeholder name; the quoted values must appear intact and no placeholder may survive outside them. Real-shell checks for csh and activate_this.py. - test_bash / test_fish: placeholder-in-parent-dir cases with a quote payload; no PWNED after sourcing (bash, zsh, fish). Without the fix: 28 failures on py3.10 and 16 on py2.7. The cases that fail differ by interpreter because the old loop followed dict order, which differs between 2.7 and 3.x. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Fix CVE-2026-115586 / GHSA-cx6f-vjhg-g3cc: escape the bash prompt for every PS1 state The GHSA-6w3r-xr62-4xcp port (7a2c40b) followed upstream pypa#3418: when bash's promptvars was off (and not in posix mode) at activation, PS1 received the raw prompt text with only backslashes doubled. promptvars can be switched on afterwards, and the exported PS1 reaches child shells that have it on, so with --prompt 'x$(touch PWNED)y': shopt -u promptvars; . bin/activate; shopt -s promptvars; echo "${PS1@P}" and shopt -u promptvars; . bin/activate; bash -i both ran the command (the child-shell case did not run on the base). Port upstream 3598e20 (pypa#3466, 21.14.13): - bash: escape the value for both passes whatever the current state: each backslash becomes four, $ and ` get a backslash. The escaped text is built one character at a time, because ${var//pat/rep} drops backslashes depending on the bash version, BASH_COMPAT and patsub_replacement. - zsh: put the value in psvar[9] and reference it as %9v. zsh inserts a psvar entry as data under every prompt option, and an exported PS1 carries nothing another shell could expand. deactivate restores psvar[9]. Tests (test_bash.py): - promptvars off at activation, then: promptvars on plus ${PS1@P}; a child bash -i; a child dash -i. The payload must render literally and create no file. - The exact stored PS1 form, under default and BASH_COMPAT=42. - zsh deactivate restores psvar. - The earlier unset-with-nounset case now expects the baked-in prompt. All six new cases fail on the previous template on py2.7 and py3.10, and the existing prompt-literal and command-substitution tests still pass. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Keep partial seed wheel images out of the app-data cache No CVE; this is a robustness fix next to GHSA-494p-63j8-28p3, raised in review. The app-data seeder extracts each seed wheel into a cached "image" directory and later runs reuse any image directory that exists and is not empty. When a build failed part way (disk full, path too long, killed process), the half-extracted image stayed in the cache, and every later environment was seeded from it, with no RECORD or INSTALLER. An empty image directory was worse: has_image() called next() on an empty iterator and crashed the seed with StopIteration. Port upstream 59edf49 (pypa#3487, 21.15.2): - build_image() extracts into a staging directory beside the installer folder, of the same path length, so Windows path limits hit the same way. - It finishes the image there, including the read-only pass for the symlink installer, then renames it into place. - On any failure the staging directory is removed, and a leftover from a killed build is removed before the next attempt. - has_image() treats an empty directory as missing. Python 2.7: os.rename instead of os.replace. An empty target directory is removed first, which keeps the rename working on Windows. No f-strings or Final. Tests: - test_seed_image_build_failure_leaves_no_partial_image: extraction failure, a finishing-step failure, and the symlink installer's read-only step. After each, the next run seeds a complete pip. - test_seed_image_build_replaces_leftovers: a killed build's staging directory, and an empty image directory. All six fail without this change on py2.7 and py3.10. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Fix CVE-2026-113713 / GHSA-4f8h-vr72-p32c: copy seed packages from temporary app-data The advisory's range is >=21.14.8, and review expected it not to apply here. It does apply. When the app-data folder is unusable, virtualenv falls back to a temporary app-data folder and removes it after creation. The --symlink-app-data parser guard turns the command-line flag into a no-op for temporary app-data. But the same option set through VIRTUALENV_SYMLINK_APP_DATA=true or `symlink_app_data = true` in virtualenv.ini still reached FromAppData unchanged. Reproduced on this branch on py2.7 and py3.10, with VIRTUALENV_OVERRIDE_APP_DATA pointing below a regular file: - site-packages/pip was a symlink into the deleted temporary folder, and `bin/pip` failed with ImportError; - any user who recreated that path would have supplied the code the environment imports. The command-line form already copied. Port upstream b81995c (pypa#3437, 21.14.10). FromAppData uses symlinks only when the app-data is not transient, and logs that it copies instead. Test: test_transient_seed_survives_cache_cleanup covers the environment, configuration and command-line forms. pip must be a real directory and `python -m pip --version` must work. The environment and configuration cases fail without this change on py2.7 and py3.10. Co-Authored-By: Claude Opus 5.5 <[email protected]> * Changelog: add GHSA-8rjx, GHSA-cx6f and GHSA-4f8h, and the seed image fix Three rows added to the v20.15.1.post3 table, all patched: - CVE-2026-113776 / GHSA-8rjx-v5ww-45pp - CVE-2026-115586 / GHSA-cx6f-vjhg-g3cc - CVE-2026-113713 / GHSA-4f8h-vr72-p32c Their published ranges leave out 20.15.1, but this line's code is affected. Also adds a bugfix bullet for the staged seed image build (upstream pypa#3487), and the summary now covers fixes up to 21.14.13. Co-Authored-By: Claude Opus 5.5 <[email protected]> --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
There was an error while loading. Please reload this page.