GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — 🐛 fix(seed): copy from temporary app-data by gaborbernat · Pull Request #3437 · pypa/virtualenv

Conversation

@gaborbernat

@gaborbernat gaborbernat commented Oct 17, 2026 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

The command-line --symlink-app-data switch is disabled when app-data is temporary, but VIRTUALENV_SYMLINK_APP_DATA and virtualenv.ini can still supply a true default. The seeder then links pip into a temporary cache that virtualenv deletes after creation, leaving a dangling package path. On a shared temporary directory, another account can claim that path before the environment owner next runs pip. 🐛 This PR copies seed packages whenever the selected app-data store is temporary. It addresses GHSA-4f8h-vr72-p32c.

The command-line guard did not cover configuration defaults

add_parser_arguments changes the switch to store_false when app-data is temporary. That guards an explicit command-line flag, but configuration and environment values can become the parser default. FromAppData.__init__ used that value without checking the selected cache lifetime. The seeder now checks app_data.transient when choosing between copying and linking, so all three option sources follow the same rule. The regression tests create an environment with an unusable persistent cache and check that pip remains available after the temporary cache is removed.

Behavior changes

When app-data is temporary, a symlink request from an environment variable or configuration file now copies seed packages and logs the fallback at debug level. Persistent app-data keeps the requested symlink behavior. Recreate environments that already contain dangling seed links.

Reported by Anthropic's OSS Scanner: ANT-2026-ZOE2GEPC.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@gaborbernat
🐛 fix(seed): copy from temporary app-data
5aa37fd
Configuration and environment defaults can enable symlink seeding with
temporary app-data. The links dangle when virtualenv removes that cache.
Choose copying when the selected app-data store is temporary.
@gaborbernat gaborbernat added the bug label Oct 17, 2026
@gaborbernat
🧪 test(seed): preserve host environment
04a3f4d
Windows Python 3.9 and 3.10 fail to start interpreter subprocesses when these tests replace the entire environment. Keep host variables while overriding the settings needed for temporary app-data.
@gaborbernat
gaborbernat merged commit b81995c into pypa:main Oct 17, 2026
76 checks passed

Uh oh!

There was an error while loading. Please reload this page.

@darrenhuai darrenhuai mentioned this pull request Oct 22, 2026
🐛 fix(seed): copy seed packages when symlinks are unsupported #3465
Merged
6 of 9 tasks
@nixpkgs-security-tracker nixpkgs-security-tracker Bot mentioned this pull request Oct 22, 2026
virtualenv: security issues < 21.14.9 NixOS/nixpkgs#577306
Open
@bboretzky

bboretzky commented Oct 22, 2026

Copy link
Copy Markdown

Thank you for the fix. A request on the advisory record: GHSA-4f8h-vr72-p32c (CVE-2026-113713) lists the affected range as >= 21.14.8 with no patched version, while this change shipped in 21.14.10 (its changelog entry). Could the advisory record 21.14.10 as the patched version?

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 >=21.14.10 floor on the strength of the changelog.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@gaborbernat
gaborbernat deleted the fix/transient-app-data-seeding branch October 22, 2026 14:49
@gaborbernat

gaborbernat commented Oct 22, 2026

Copy link
Copy Markdown
Contributor Author

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.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@StrudelDoodleS StrudelDoodleS mentioned this pull request Oct 22, 2026
virtualenv 21.14.9 for its high-severity launcher advisory StrudelDoodleS/superglm#517
Merged
@shimi3435 shimi3435 mentioned this pull request Oct 23, 2026
fix: virtualenvの追加セキュリティ修正を反映 shimi3435/ai-coding-template-ja#93
Merged
This was referenced Oct 24, 2026
[Cycode] Fix for vulnerable manifest file dependency - virtualenv updated to version 21.14.9 TannaCycode/cicd-goat#724
Open
[Cycode] Fix for vulnerable manifest file dependency - virtualenv updated to version 21.14.9 TannaCycode/cicd-goat#725
Open
[Cycode] Fix for vulnerable manifest file dependency - virtualenv updated to version 21.14.9 TannaCycode/cicd-goat#726
Open
[Cycode] Fix for vulnerable manifest file dependency - virtualenv updated to version 21.14.9 TannaCycode/cicd-goat#727
Open
[Cycode] Fix for vulnerable manifest file dependency - virtualenv updated to version 21.14.9 TannaCycode/cicd-goat#728
Open
[Cycode] Fix for vulnerable manifest file dependency - virtualenv updated to version 21.14.9 TannaCycode/cicd-goat#729
Open
[Cycode] Fix for vulnerable manifest file dependency - virtualenv updated to version 21.14.9 TannaCycode/cicd-goat#730
Open
[Cycode] Fix for vulnerable manifest file dependency - virtualenv updated to version 21.14.9 TannaCycode/cicd-goat#731
Open
[Cycode] Fix for vulnerable manifest file dependency - virtualenv updated to version 21.14.9 TannaCycode/cicd-goat#732
Open
[Cycode] Fix for vulnerable manifest file dependency - virtualenv updated to version 21.14.9 TannaCycode/cicd-goat#733
Open
[Cycode] Fix for vulnerable manifest file dependency - virtualenv updated to version 21.14.9 TannaCycode/cicd-goat#734
Open
[Cycode] Fix for vulnerable manifest file dependency - virtualenv updated to version 21.14.9 TannaCycode/cicd-goat#735
Open
[Cycode] Fix for vulnerable manifest file dependency - virtualenv updated to version 21.14.9 TannaCycode/cicd-goat#736
Open
[Cycode] Fix for vulnerable manifest file dependency - virtualenv updated to version 21.14.9 TannaCycode/cicd-goat#737
Open
[Cycode] Fix for vulnerable manifest file dependency - virtualenv updated to version 21.14.9 TannaCycode/cicd-goat#738
Open
[Cycode] Fix for vulnerable manifest file dependency - virtualenv updated to version 21.14.9 TannaCycode/cicd-goat#739
Open
[Cycode] Fix for vulnerable manifest file dependency - virtualenv updated to version 21.14.9 TannaCycode/cicd-goat#740
Open
[Cycode] Fix for vulnerable manifest file dependency - virtualenv updated to version 21.14.9 TannaCycode/cicd-goat#741
Open
[Cycode] Fix for vulnerable manifest file dependency - virtualenv updated to version 21.14.9 TannaCycode/cicd-goat#742
Open
This was referenced Oct 24, 2026
[Cycode] Fix for vulnerable manifest file dependency - virtualenv updated to version 21.14.9 TannaCycode/cicd-goat#743
Open
[Cycode] Fix for vulnerable manifest file dependency - virtualenv updated to version 21.14.9 TannaCycode/cicd-goat#744
Open
[Cycode] Fix for vulnerable manifest file dependency - virtualenv updated to version 21.14.9 TannaCycode/cicd-goat#745
Open
[Cycode] Fix for vulnerable manifest file dependency - virtualenv updated to version 21.14.9 TannaCycode/cicd-goat#746
Open
[Cycode] Fix for vulnerable manifest file dependency - virtualenv updated to version 21.14.9 TannaCycode/cicd-goat#747
Open
[Cycode] Fix for vulnerable manifest file dependency - virtualenv updated to version 21.14.9 TannaCycode/cicd-goat#748
Open
[Cycode] Fix for vulnerable manifest file dependency - virtualenv updated to version 21.14.9 TannaCycode/cicd-goat#749
Open
[Cycode] Fix for vulnerable manifest file dependency - virtualenv updated to version 21.14.9 TannaCycode/cicd-goat#750
Open
[Cycode] Fix for vulnerable manifest file dependency - virtualenv updated to version 21.14.9 TannaCycode/cicd-goat#751
Open
icanhasmath added a commit to ActiveState/virtualenv that referenced this pull request Nov 5, 2026
@icanhasmath @claude
Fix CVE-2026-113713 / GHSA-4f8h-vr72-p32c: copy seed packages from te…
31254fb
…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]>
@icanhasmath icanhasmath mentioned this pull request Nov 5, 2026
Release 20.15.1.post3: backport fixes for 18 virtualenv security advisories ActiveState/virtualenv#3
Merged
icanhasmath added a commit to ActiveState/virtualenv that referenced this pull request Nov 5, 2026
@icanhasmath @claude
Release 20.15.1.post3: backport fixes for 18 virtualenv security advi…
b4d3543
…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]>
@github-actions github-actions Bot mentioned this pull request Nov 6, 2026
[candidate] CVE-2026-113713 (via GHSA-4f8h-vr72-p32c) patrickmgarrity/Anthropic-Credited-CVEs#780
Merged
1 task done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

No reviews

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

bug

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

2 participants

@gaborbernat @bboretzky
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.