diff mbox series

[meta-python,wrynose,04/18] python3-virtualenv: fix CVE-2026-113732

Message ID 20261105090007.351866-5-hthakar@cisco.com
State New
Headers show
Series python3-virtualenv: Security fixes | expand

Commit Message

From: Hetvi Thakar <hthakar@cisco.com>

This patch applies the upstream fix as referenced in [2], using the
commit shown in [1]. Use Fish-specific quoting so backslashes and
quotes in an environment path or prompt remain literal data.

Include follow-up [3], released in [4], to preserve apostrophes and
backslashes after multibyte text in Fish 3 legacy locales. Keep the
upstream GBK and UTF-8 regression tests.

[1] https://github.com/pypa/virtualenv/commit/3bddeaf8d02c3ee7f576503efb082f8f494cfcab
[2] https://github.com/pypa/virtualenv/security/advisories/GHSA-c947-3pg5-gm8q
[3] https://github.com/pypa/virtualenv/commit/685435d7fb53da12da99e195ee2753f5ca083249
[4] https://github.com/pypa/virtualenv/releases/tag/21.14.9

Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
---
 .../CVE-2026-113732_p1.patch                  | 116 +++++++++++++++++
 .../CVE-2026-113732_p2.patch                  | 120 ++++++++++++++++++
 .../python/python3-virtualenv_21.2.1.bb       |   2 +
 3 files changed, 238 insertions(+)
 create mode 100644 meta-python/recipes-devtools/python/python3-virtualenv/CVE-2026-113732_p1.patch
 create mode 100644 meta-python/recipes-devtools/python/python3-virtualenv/CVE-2026-113732_p2.patch
diff mbox series

Patch

diff --git a/meta-python/recipes-devtools/python/python3-virtualenv/CVE-2026-113732_p1.patch b/meta-python/recipes-devtools/python/python3-virtualenv/CVE-2026-113732_p1.patch
new file mode 100644
index 0000000000..1afedad035
--- /dev/null
+++ b/meta-python/recipes-devtools/python/python3-virtualenv/CVE-2026-113732_p1.patch
@@ -0,0 +1,116 @@ 
+From 3bddeaf8d02c3ee7f576503efb082f8f494cfcab Mon Sep 17 00:00:00 2001
+From: Bernát Gábor <gaborjbernat@gmail.com>
+Date: Wed, 30 Sep 2026 22:59:45 -0700
+Subject: [PATCH] Merge commit from fork
+
+FishActivator inherited shlex.quote, whose POSIX rules leave a backslash
+literal inside single quotes. fish reads \' there as an escaped quote, so a
+path or --prompt with a backslash before a quote ended the literal early and
+ran the rest as commands, without any error from fish.
+
+Give FishActivator its own quote() that escapes backslashes and quotes.
+
+CVE: CVE-2026-113732
+Upstream-Status: Backport [https://github.com/pypa/virtualenv/commit/3bddeaf8d02c3ee7f576503efb082f8f494cfcab]
+
+Backport Changes:
+- Add the cli_run import required by the upstream regression test;
+  it is absent from the 21.2.1 test module.
+
+Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
+---
+ src/virtualenv/activation/fish/__init__.py |  6 ++++
+ tests/unit/activation/test_fish.py         | 40 ++++++++++++++++++++--
+ 2 files changed, 43 insertions(+), 3 deletions(-)
+
+diff --git a/src/virtualenv/activation/fish/__init__.py b/src/virtualenv/activation/fish/__init__.py
+index 74300db..81d6d92 100644
+--- a/src/virtualenv/activation/fish/__init__.py
++++ b/src/virtualenv/activation/fish/__init__.py
+@@ -15,6 +15,12 @@ class FishActivator(ViaTemplateActivator):
+     def templates(self) -> Iterator[str]:
+         yield "activate.fish"
+ 
++    @staticmethod
++    def quote(string: str) -> str:
++        # fish reads \\ and \' as escapes inside single quotes, unlike POSIX sh, so shlex.quote's output lets a
++        # backslash before a quote end the literal early
++        return "'" + string.replace("\\", "\\\\").replace("'", "\\'") + "'"
++
+     def replacements(self, creator: Creator, dest_folder: Path) -> dict[str, str]:
+         data = super().replacements(creator, dest_folder)
+         data.update({
+diff --git a/tests/unit/activation/test_fish.py b/tests/unit/activation/test_fish.py
+index f726e7e..fc1b8a6 100644
+--- a/tests/unit/activation/test_fish.py
++++ b/tests/unit/activation/test_fish.py
+@@ -1,7 +1,6 @@
+ from __future__ import annotations
+ 
+ import os
+-import shlex
+ import shutil
+ import subprocess
+ import sys
+@@ -13,6 +12,7 @@ import pytest
+ 
+ from virtualenv.activation import FishActivator
+ from virtualenv.info import IS_WIN
++from virtualenv.run import cli_run
+ 
+ if TYPE_CHECKING:
+     from collections.abc import Callable
+@@ -59,8 +59,8 @@ def test_fish_tkinter_generation(
+     assert "set -e _OLD_PKG_CONFIG_PATH" in content
+ 
+     if present:
+-        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
++        assert f"set -gx TCL_LIBRARY {FishActivator.quote(tcl_lib)}\n" in content
++        assert f"set -gx TK_LIBRARY {FishActivator.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
+@@ -79,6 +79,40 @@ def test_fish_tkinter_path_does_not_run_commands(
+     assert not marker.exists()
+ 
+ 
++@pytest.mark.skipif(IS_WIN, reason="fish is not available on Windows")
++@pytest.mark.skipif(FISH is None, reason="fish is not installed")
++@pytest.mark.parametrize(
++    ("prompt", "env_name"),
++    [
++        pytest.param(r"x\'$(touch PWNED)'\'", "env", id="prompt-backslash-quote"),
++        pytest.param(None, r"x\'$(touch PWNED)'\'", id="dirname-backslash-quote"),
++    ],
++)
++def test_fish_activate_does_not_run_commands(
++    tmp_path: Path, current_fastest: str, prompt: str | None, env_name: str
++) -> None:
++    dest = tmp_path / env_name
++    args = ["--without-pip", str(dest), "--creator", current_fastest, "--no-periodic-update", "--activators", "fish"]
++    if prompt is not None:
++        args += ["--prompt", prompt]
++    cli_run(args)
++    (work_dir := tmp_path / "workdir").mkdir()
++
++    # the path is passed as an argument so the driver cannot expand a payload dir name while locating the script
++    result = subprocess.run(
++        [FISH, "--no-config", "-c", 'source $argv[1]; printf "%s" "$VIRTUAL_ENV"', str(dest / "bin" / "activate.fish")],
++        capture_output=True,
++        text=True,
++        encoding="utf-8",
++        cwd=str(work_dir),
++        timeout=60,
++        check=False,
++    )
++
++    assert not (work_dir / "PWNED").exists()
++    assert result.stdout == str(dest), result.stderr
++
++
+ @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/CVE-2026-113732_p2.patch b/meta-python/recipes-devtools/python/python3-virtualenv/CVE-2026-113732_p2.patch
new file mode 100644
index 0000000000..ad66d9b1e2
--- /dev/null
+++ b/meta-python/recipes-devtools/python/python3-virtualenv/CVE-2026-113732_p2.patch
@@ -0,0 +1,120 @@ 
+From 685435d7fb53da12da99e195ee2753f5ca083249 Mon Sep 17 00:00:00 2001
+From: Bernát Gábor <gaborjbernat@gmail.com>
+Date: Sat, 17 Oct 2026 02:08:22 -0700
+Subject: [PATCH]