From abb85935fe1ce984c28e7074a56df1f9b5fa68db Mon Sep 17 00:00:00 2001 From: Hetvi Thakar Date: Thu, 5 Nov 2026 00:59:52 -0800 Subject: [PATCH] python3-virtualenv: fix CVE-2026-102925 This patch applies the upstream fix as referenced in [2], using the commit shown in [1]. Remove the extra template quotes around values that are already quoted for Bash and Fish activation scripts. [1] https://github.com/pypa/virtualenv/commit/4d5a105ec2a2723b8c7f4571bb68f4f71d01e3f6 [2] https://github.com/pypa/virtualenv/security/advisories/GHSA-p58f-9548-mpm2 Signed-off-by: Hetvi Thakar Signed-off-by: Anuj Mittal --- .../python3-virtualenv/CVE-2026-102925.patch | 296 ++++++++++++++++++ .../python/python3-virtualenv_21.2.1.bb | 1 + 2 files changed, 297 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-virtualenv/CVE-2026-102925.patch diff --git a/meta-python/recipes-devtools/python/python3-virtualenv/CVE-2026-102925.patch b/meta-python/recipes-devtools/python/python3-virtualenv/CVE-2026-102925.patch new file mode 100644 index 0000000000..f9f6747dd9 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-virtualenv/CVE-2026-102925.patch @@ -0,0 +1,296 @@ +From 4d5a105ec2a2723b8c7f4571bb68f4f71d01e3f6 Mon Sep 17 00:00:00 2001 +From: Bernát Gábor +Date: Fri, 18 Sep 2026 06:10:09 +0200 +Subject: [PATCH] 🐛 fix(activation): stop path command injection in bash and + fish (#3252) +MIME-Version: 1.0 +Content-Type: text/plain; charset=UTF-8 +Content-Transfer-Encoding: 8bit + +CVE: CVE-2026-102925 +Upstream-Status: Backport [https://github.com/pypa/virtualenv/commit/4d5a105ec2a2723b8c7f4571bb68f4f71d01e3f6] + +Backport Changes: +- Add the shutil and subprocess imports and FISH executable lookup + required by the upstream Fish regression tests; these are absent from + the 21.2.1 test module. +- Omit the release-only changelog fragment; the release documentation + directory is not included in the 21.2.1 source distribution. + +Signed-off-by: Hetvi Thakar +--- + src/virtualenv/activation/bash/activate.sh | 2 +- + src/virtualenv/activation/fish/activate.fish | 4 +- + tests/unit/activation/test_bash.py | 85 ++++++++++++++------ + tests/unit/activation/test_fish.py | 82 ++++++++++++------- + 4 files changed, 117 insertions(+), 56 deletions(-) + +diff --git a/src/virtualenv/activation/bash/activate.sh b/src/virtualenv/activation/bash/activate.sh +index b418101..18a78e5 100644 +--- a/src/virtualenv/activation/bash/activate.sh ++++ b/src/virtualenv/activation/bash/activate.sh +@@ -61,7 +61,7 @@ deactivate () { + deactivate nondestructive + + if [ ! -d __VIRTUAL_ENV__ ]; then +- echo "Virtual environment directory __VIRTUAL_ENV__ does not exist!" >&2 ++ echo "Virtual environment directory" __VIRTUAL_ENV__ "does not exist!" >&2 + CURRENT_PATH=$(realpath "${BASH_SOURCE[0]}") + CURRENT_DIR=$(dirname "${CURRENT_PATH}") + VIRTUAL_ENV="$(realpath "${CURRENT_DIR}/../")" +diff --git a/src/virtualenv/activation/fish/activate.fish b/src/virtualenv/activation/fish/activate.fish +index 33a8c7f..e409de9 100644 +--- a/src/virtualenv/activation/fish/activate.fish ++++ b/src/virtualenv/activation/fish/activate.fish +@@ -72,13 +72,13 @@ if test -n __TCL_LIBRARY__ + if set -q TCL_LIBRARY; + set -gx _OLD_VIRTUAL_TCL_LIBRARY $TCL_LIBRARY; + end +- set -gx TCL_LIBRARY '__TCL_LIBRARY__' ++ set -gx TCL_LIBRARY __TCL_LIBRARY__ + end + if test -n __TK_LIBRARY__ + if set -q TK_LIBRARY; + set -gx _OLD_VIRTUAL_TK_LIBRARY $TK_LIBRARY; + end +- set -gx TK_LIBRARY '__TK_LIBRARY__' ++ set -gx TK_LIBRARY __TK_LIBRARY__ + end + + # Prompt override provided? +diff --git a/tests/unit/activation/test_bash.py b/tests/unit/activation/test_bash.py +index 7a1bd35..00ac55f 100644 +--- a/tests/unit/activation/test_bash.py ++++ b/tests/unit/activation/test_bash.py +@@ -3,6 +3,7 @@ from __future__ import annotations + import shutil + import subprocess + from argparse import Namespace ++from typing import TYPE_CHECKING + + import pytest + +@@ -10,6 +11,10 @@ from virtualenv.activation import BashActivator + from virtualenv.info import IS_WIN + from virtualenv.run import cli_run + ++if TYPE_CHECKING: ++ from collections.abc import Callable ++ from pathlib import Path ++ + + @pytest.mark.skipif(IS_WIN, reason="Github Actions ships with WSL bash") + @pytest.mark.parametrize( +@@ -73,34 +78,68 @@ def test_bash_tkinter_generation(tmp_path, tcl_lib, tk_lib, present) -> None: + assert "export TCL_LIBRARY" in content + + ++@pytest.fixture ++def relocated_bash_venv( ++ tmp_path: Path, current_fastest: str ++) -> Callable[[str], tuple[subprocess.CompletedProcess[str], Path, Path]]: ++ def source_after_move(name: str) -> tuple[subprocess.CompletedProcess[str], Path, Path]: ++ original = tmp_path / name ++ cli_run([ ++ "--without-pip", ++ str(original), ++ "--creator", ++ current_fastest, ++ "--no-periodic-update", ++ "--activators", ++ "bash", ++ ]) ++ relocated = tmp_path / "relocated" ++ shutil.move(original, relocated) ++ work_dir = tmp_path / "workdir" ++ work_dir.mkdir() ++ result = subprocess.run( ++ ["bash", "-c", f'source "{relocated / "bin" / "activate"}" 2>/dev/null && echo "$VIRTUAL_ENV"'], ++ capture_output=True, ++ text=True, ++ cwd=str(work_dir), ++ encoding="utf-8", ++ check=False, ++ ) ++ return result, relocated, work_dir ++ ++ return source_after_move ++ ++ + @pytest.mark.skipif(IS_WIN, reason="Github Actions ships with WSL bash") +-def test_bash_activate_relocation_resolves_virtual_env(tmp_path, current_fastest) -> None: +- original = tmp_path / "original" +- cli_run([ +- "--without-pip", +- str(original), +- "--creator", +- current_fastest, +- "--no-periodic-update", +- "--activators", +- "bash", +- ]) +- relocated = tmp_path / "relocated" +- shutil.move(original, relocated) +- +- work_dir = tmp_path / "workdir" +- work_dir.mkdir() +- activate_script = relocated / "bin" / "activate" +- result = subprocess.run( +- ["bash", "-c", f'source "{activate_script}" 2>/dev/null && echo "$VIRTUAL_ENV"'], +- capture_output=True, +- text=True, +- cwd=str(work_dir), +- ) ++@pytest.mark.parametrize( ++ "name", [pytest.param("original", id="plain"), pytest.param("has(paren)and'quote", id="shell-metacharacters")] ++) ++def test_bash_activate_relocation_resolves_virtual_env( ++ relocated_bash_venv: Callable[[str], tuple[subprocess.CompletedProcess[str], Path, Path]], name: str ++) -> None: ++ result, relocated, _ = relocated_bash_venv(name) ++ + assert result.returncode == 0 + assert result.stdout.strip() == str(relocated) + + ++@pytest.mark.skipif(IS_WIN, reason="Github Actions ships with WSL bash") ++@pytest.mark.parametrize( ++ "payload", ++ [ ++ pytest.param("x'$(id > PWNED)'y", id="command-substitution"), ++ pytest.param("x'`id > PWNED`'y", id="backticks"), ++ pytest.param("a';id > PWNED;'b", id="statement-separator"), ++ ], ++) ++def test_bash_activate_relocation_does_not_run_path_commands( ++ relocated_bash_venv: Callable[[str], tuple[subprocess.CompletedProcess[str], Path, Path]], payload: str ++) -> None: ++ _, _, work_dir = relocated_bash_venv(payload) ++ ++ assert not (work_dir / "PWNED").exists() ++ ++ + @pytest.mark.skipif(IS_WIN, reason="Github Actions ships with WSL bash") + @pytest.mark.parametrize("hashing_enabled", [True, False]) + def test_bash(raise_on_non_source_class, hashing_enabled, activation_tester) -> None: +diff --git a/tests/unit/activation/test_fish.py b/tests/unit/activation/test_fish.py +index 4d28b78..f726e7e 100644 +--- a/tests/unit/activation/test_fish.py ++++ b/tests/unit/activation/test_fish.py +@@ -1,62 +1,84 @@ + from __future__ import annotations + + import os ++import shlex ++import shutil ++import subprocess + import sys + from argparse import Namespace ++from types import SimpleNamespace ++from typing import TYPE_CHECKING + + import pytest + + from virtualenv.activation import FishActivator + from virtualenv.info import IS_WIN + ++if TYPE_CHECKING: ++ from collections.abc import Callable ++ from pathlib import Path ++ ++FISH = shutil.which("fish") ++ ++ ++@pytest.fixture ++def rendered_activate_fish(tmp_path: Path) -> Callable[[str | None, str | None], Path]: ++ def render(tcl_lib: str | None, tk_lib: str | None) -> Path: ++ creator = SimpleNamespace( ++ dest=tmp_path, ++ bin_dir=tmp_path / "bin", ++ interpreter=SimpleNamespace(tcl_lib=tcl_lib, tk_lib=tk_lib), ++ pyenv_cfg={}, ++ env_name="my-env", ++ ) ++ creator.bin_dir.mkdir() ++ FishActivator(Namespace(prompt=None)).generate(creator) ++ return creator.bin_dir / "activate.fish" ++ ++ return render ++ + + @pytest.mark.parametrize( + ("tcl_lib", "tk_lib", "present"), + [ + ("/path/to/tcl", "/path/to/tk", True), ++ ("/Program Files/tcl", "/Program Files/tk", True), + (None, None, False), + ], + ) +-def test_fish_tkinter_generation(tmp_path, tcl_lib, tk_lib, present) -> None: +- # GIVEN +- class MockInterpreter: +- pass +- +- interpreter = MockInterpreter() +- interpreter.tcl_lib = tcl_lib +- interpreter.tk_lib = tk_lib +- +- class MockCreator: +- def __init__(self, dest) -> None: +- self.dest = dest +- self.bin_dir = dest / "bin" +- self.bin_dir.mkdir() +- self.interpreter = interpreter +- self.pyenv_cfg = {} +- self.env_name = "my-env" +- +- creator = MockCreator(tmp_path) +- options = Namespace(prompt=None) +- activator = FishActivator(options) +- +- # WHEN +- activator.generate(creator) +- content = (creator.bin_dir / "activate.fish").read_text(encoding="utf-8") +- +- # THEN +- # PKG_CONFIG_PATH is always set ++def test_fish_tkinter_generation( ++ rendered_activate_fish: Callable[[str | None, str | None], Path], ++ tcl_lib: str | None, ++ tk_lib: str | None, ++ present: bool, ++) -> None: ++ content = rendered_activate_fish(tcl_lib, tk_lib).read_text(encoding="utf-8") ++ + assert 'set -gx _OLD_PKG_CONFIG_PATH "$PKG_CONFIG_PATH"' in content + assert 'set -gx PKG_CONFIG_PATH "$VIRTUAL_ENV/lib/pkgconfig:$PKG_CONFIG_PATH"' in content + assert "set -e _OLD_PKG_CONFIG_PATH" in content + + if present: +- assert "set -gx TCL_LIBRARY '/path/to/tcl'" in content +- assert "set -gx TK_LIBRARY '/path/to/tk'" in content ++ assert f"set -gx TCL_LIBRARY {shlex.quote(tcl_lib)}\n" in content ++ assert f"set -gx TK_LIBRARY {shlex.quote(tk_lib)}\n" in content + else: + assert "if test -n ''\n if set -q TCL_LIBRARY;" in content + assert "if test -n ''\n if set -q TK_LIBRARY;" in content + + ++@pytest.mark.skipif(IS_WIN, reason="fish is not available on Windows") ++@pytest.mark.skipif(FISH is None, reason="fish is not installed") ++def test_fish_tkinter_path_does_not_run_commands( ++ rendered_activate_fish: Callable[[str | None, str | None], Path], tmp_path: Path ++) -> None: ++ marker = tmp_path / "PWNED" ++ script = rendered_activate_fish(f"/tcl/(touch {marker})/lib", "/tk/lib") ++ ++ subprocess.run([FISH, "-c", f"source '{script}'"], capture_output=True, text=True, timeout=60, check=False) ++ ++ assert not marker.exists() ++ ++ + @pytest.mark.skipif(IS_WIN, reason="we have not setup fish in CI yet") + def test_fish(activation_tester_class, activation_tester, monkeypatch, tmp_path) -> None: + monkeypatch.setenv("HOME", str(tmp_path)) +-- +2.35.6 diff --git a/meta-python/recipes-devtools/python/python3-virtualenv_21.2.1.bb b/meta-python/recipes-devtools/python/python3-virtualenv_21.2.1.bb index 397c8bfafc..756a7842b8 100644 --- a/meta-python/recipes-devtools/python/python3-virtualenv_21.2.1.bb +++ b/meta-python/recipes-devtools/python/python3-virtualenv_21.2.1.bb @@ -14,6 +14,7 @@ SRC_URI += " \ file://CVE-2026-102930_p1.patch \ file://CVE-2026-102930_p2.patch \ file://CVE-2026-102930_p3.patch \ + file://CVE-2026-102925.patch \ " BBCLASSEXTEND = "native nativesdk"