From 936c7322528c60ee9055f831f33c019824d73a07 Mon Sep 17 00:00:00 2001 From: Hetvi Thakar Date: Thu, 5 Nov 2026 00:59:54 -0800 Subject: [PATCH] python3-virtualenv: fix CVE-2026-113776 This patch applies the upstream fix as referenced in [2], using the commit shown in [1]. Replace activation-template placeholders in a single pass so placeholder names inside substituted paths or prompts cannot be expanded again as script content. [1] https://github.com/pypa/virtualenv/commit/602b26623a724827ff171bf11fef4ad186d01f74 [2] https://github.com/pypa/virtualenv/security/advisories/GHSA-8rjx-v5ww-45pp Signed-off-by: Hetvi Thakar Signed-off-by: Anuj Mittal --- .../python3-virtualenv/CVE-2026-113776.patch | 133 ++++++++++++++++++ .../python/python3-virtualenv_21.2.1.bb | 1 + 2 files changed, 134 insertions(+) create mode 100644 meta-python/recipes-devtools/python/python3-virtualenv/CVE-2026-113776.patch diff --git a/meta-python/recipes-devtools/python/python3-virtualenv/CVE-2026-113776.patch b/meta-python/recipes-devtools/python/python3-virtualenv/CVE-2026-113776.patch new file mode 100644 index 0000000000..c1e034edb5 --- /dev/null +++ b/meta-python/recipes-devtools/python/python3-virtualenv/CVE-2026-113776.patch @@ -0,0 +1,133 @@ +From 602b26623a724827ff171bf11fef4ad186d01f74 Mon Sep 17 00:00:00 2001 +From: Bernát Gábor +Date: Wed, 30 Sep 2026 23:01:05 -0700 +Subject: [PATCH] Merge commit from fork + +instantiate_template replaced one placeholder at a time across the whole +template, so a placeholder name inside an already quoted value, such as a +parent directory named __VIRTUAL_NAME__, was rewritten from inside the +finished literal. The second value's quotes closed the first, and the rest +of the path ran as code in bash, fish, xonsh and activate_this.py; in csh +the prompt display tokens broke activate.csh the same way. + +Build every token's final text first and substitute them in one regex pass, +with the csh display tokens joining that pass through substitutions(). + +CVE: CVE-2026-113776 +Upstream-Status: Backport [https://github.com/pypa/virtualenv/commit/602b26623a724827ff171bf11fef4ad186d01f74] + +Backport Changes: +- Omit the Csh display-token override and its specific test: 21.2.1 has + no separate display-token substitution or prompt-literal helpers. + Its Csh activator uses the corrected shared template substitution. +- Exercise upstream's placeholder-in-parent-directory regression through + the existing Bash relocation fixture, since 21.2.1 lacks the later + prompt-test fixture. Add direct one-pass substitution checks for the + existing Bash, Csh, Fish and Python activators, and retain the 21.2.1 + special-character fixture's other characters. + +Signed-off-by: Hetvi Thakar +--- + src/virtualenv/activation/via_template.py | 21 +++++++++++++++++---- + tests/conftest.py | 3 ++- + tests/unit/activation/test_activator.py | 16 ++++++++++++++++ + tests/unit/activation/test_bash.py | 1 + + 4 files changed, 36 insertions(+), 5 deletions(-) + +diff --git a/src/virtualenv/activation/via_template.py b/src/virtualenv/activation/via_template.py +index 926c2b0..3af9dc5 100644 +--- a/src/virtualenv/activation/via_template.py ++++ b/src/virtualenv/activation/via_template.py +@@ -1,6 +1,7 @@ + from __future__ import annotations + + import os ++import re + import shlex + import sys + from abc import ABC, abstractmethod +@@ -84,10 +85,22 @@ class ViaTemplateActivator(Activator, ABC): + # read content as binary to avoid platform specific line normalization (\n -> \r\n) + binary = read_binary(self.__module__, template) + text = binary.decode("utf-8", errors="strict") +- for key, value in replacements.items(): +- value_uni = self._repr_unicode(creator, value) +- text = text.replace(key, self.quote(value_uni)) +- return text ++ substitutions = self.substitutions(replacements, creator) ++ # one pass over the template, so a placeholder name inside an already quoted value (a path component named ++ # __VIRTUAL_NAME__) stays data instead of being rewritten by a later key from inside the finished literal ++ pattern = re.compile("|".join(re.escape(key) for key in sorted(substitutions, key=len, reverse=True))) ++ return pattern.sub(lambda match: substitutions[match.group()], text) ++ ++ def substitutions(self, replacements: dict[str, str], creator: Creator) -> dict[str, str]: ++ """Map each placeholder to the final text that replaces it in the template. ++ ++ :param replacements: the raw placeholder values ++ :param creator: the creator of the environment ++ ++ :returns: the placeholder to substituted text mapping ++ ++ """ ++ return {key: self.quote(self._repr_unicode(creator, value)) for key, value in replacements.items()} + + @staticmethod + def _repr_unicode(creator: Creator, value: str) -> str: # noqa: ARG004 +diff --git a/tests/conftest.py b/tests/conftest.py +index 3ec4147..f27c450 100644 +--- a/tests/conftest.py ++++ b/tests/conftest.py +@@ -279,7 +279,8 @@ def is_inside_ci(): + + @pytest.fixture(scope="session") + def special_char_name(): +- base = "'\";&&e-$ èрт🚒♞中片-j" ++ # __VIRTUAL_NAME__ is a template placeholder, and must stay data when the path embeds it ++ base = "'\";&&e-$ __VIRTUAL_NAME__èрт🚒♞中片-j" + if IS_WIN: + # get rid of invalid characters on Windows + base = base.replace('"', "") +diff --git a/tests/unit/activation/test_activator.py b/tests/unit/activation/test_activator.py +index 21be96d..03481c2 100644 +--- a/tests/unit/activation/test_activator.py ++++ b/tests/unit/activation/test_activator.py +@@ -4,9 +4,25 @@ from argparse import Namespace + + import pytest + ++from virtualenv.activation import BashActivator, CShellActivator, FishActivator, PythonActivator + from virtualenv.activation.activator import Activator + + ++@pytest.mark.parametrize("activator_class", [BashActivator, CShellActivator, FishActivator, PythonActivator]) ++def test_template_substitution_does_not_expand_replacement_values(monkeypatch, activator_class) -> None: ++ template = b"__VIRTUAL_ENV__ __VIRTUAL_NAME__" ++ monkeypatch.setattr("virtualenv.activation.via_template.read_binary", lambda *_: template) ++ activator = activator_class(Namespace(prompt=None)) ++ replacements = { ++ "__VIRTUAL_ENV__": "A__VIRTUAL_NAME__B/x'$(touch PWNED)'y", ++ "__VIRTUAL_NAME__": "x'$(touch PWNED)'y", ++ } ++ ++ actual = activator.instantiate_template(replacements, "unused", None) ++ ++ assert actual == " ".join(activator.quote(value) for value in replacements.values()) ++ ++ + @pytest.mark.graalpy + def test_activator_prompt_cwd(monkeypatch, tmp_path) -> None: + class FakeActivator(Activator): +diff --git a/tests/unit/activation/test_bash.py b/tests/unit/activation/test_bash.py +index 00ac55f..df3aaf3 100644 +--- a/tests/unit/activation/test_bash.py ++++ b/tests/unit/activation/test_bash.py +@@ -130,6 +130,7 @@ def test_bash_activate_relocation_resolves_virtual_env( + 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"), ++ pytest.param("A__VIRTUAL_NAME__B/x'$(touch PWNED)'y", id="placeholder-in-parent-dir"), + ], + ) + def test_bash_activate_relocation_does_not_run_path_commands( +-- +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 66354956b9..65e50d3f88 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 @@ -17,6 +17,7 @@ SRC_URI += " \ file://CVE-2026-102925.patch \ file://CVE-2026-113732_p1.patch \ file://CVE-2026-113732_p2.patch \ + file://CVE-2026-113776.patch \ " BBCLASSEXTEND = "native nativesdk"