mirror of
https://github.com/openembedded/meta-openembedded.git
synced 2026-11-13 15:47:23 +00:00
python3-virtualenv: fix CVE-2026-113732
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> Signed-off-by: Anuj Mittal <anuj.mittal@oss.qualcomm.com>
This commit is contained in:
1 parent
abb85935fe
commit
ad862d2df7
3 files changed
+238
No files matched your search
@@ -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
|
||||
@@ -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] 🐛 fix(fish): separate escapes from multibyte text (#3413)
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
CVE: CVE-2026-113732
|
||||
Upstream-Status: Backport [https://github.com/pypa/virtualenv/commit/685435d7fb53da12da99e195ee2753f5ca083249]
|
||||
|
||||
Backport Changes:
|
||||
- Omit the release-only changelog fragment; the release documentation
|
||||
directory is absent from the 21.2.1 source distribution.
|
||||
|
||||
Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
|
||||
---
|
||||
src/virtualenv/activation/fish/__init__.py | 6 +--
|
||||
tests/unit/activation/test_fish.py | 56 +++++++++++++++++++++-
|
||||
2 files changed, 58 insertions(+), 4 deletions(-)
|
||||
|
||||
diff --git a/src/virtualenv/activation/fish/__init__.py b/src/virtualenv/activation/fish/__init__.py
|
||||
index 81d6d92..7f376a9 100644
|
||||
--- a/src/virtualenv/activation/fish/__init__.py
|
||||
+++ b/src/virtualenv/activation/fish/__init__.py
|
||||
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
+import re
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from virtualenv.activation.via_template import ViaTemplateActivator
|
||||
@@ -17,9 +18,8 @@ class FishActivator(ViaTemplateActivator):
|
||||
|
||||
@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("'", "\\'") + "'"
|
||||
+ # Keep escapes beside ASCII quotes so legacy decoders cannot absorb them into a preceding multibyte character.
|
||||
+ return "'" + re.sub(r"[\\']", lambda match: f"'\\{match.group()}'", string) + "'"
|
||||
|
||||
def replacements(self, creator: Creator, dest_folder: Path) -> dict[str, str]:
|
||||
data = super().replacements(creator, dest_folder)
|
||||
diff --git a/tests/unit/activation/test_fish.py b/tests/unit/activation/test_fish.py
|
||||
index fc1b8a6..d131043 100644
|
||||
--- a/tests/unit/activation/test_fish.py
|
||||
+++ b/tests/unit/activation/test_fish.py
|
||||
@@ -6,7 +6,7 @@ import subprocess
|
||||
import sys
|
||||
from argparse import Namespace
|
||||
from types import SimpleNamespace
|
||||
-from typing import TYPE_CHECKING
|
||||
+from typing import TYPE_CHECKING, Final
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -21,6 +21,60 @@ if TYPE_CHECKING:
|
||||
FISH = shutil.which("fish")
|
||||
|
||||
|
||||
+@pytest.mark.skipif(IS_WIN or FISH is None, reason="fish is required")
|
||||
+@pytest.mark.parametrize(
|
||||
+ "value",
|
||||
+ [
|
||||
+ pytest.param("", id="empty"),
|
||||
+ pytest.param("plain text", id="space"),
|
||||
+ pytest.param("café's", id="unicode-apostrophe"),
|
||||
+ pytest.param("café\\path", id="unicode-backslash"),
|
||||
+ ],
|
||||
+)
|
||||
+def test_fish_quote_preserves_literal(value: str) -> None:
|
||||
+ result: Final = subprocess.run(
|
||||
+ [FISH, "--no-config"],
|
||||
+ input=f"printf '%s' {FishActivator.quote(value)}\n",
|
||||
+ capture_output=True,
|
||||
+ text=True,
|
||||
+ encoding="utf-8",
|
||||
+ timeout=30,
|
||||
+ check=True,
|
||||
+ )
|
||||
+ assert result.stdout == value
|
||||
+
|
||||
+
|
||||
+@pytest.mark.skipif(IS_WIN or FISH is None, reason="fish is required")
|
||||
+@pytest.mark.parametrize("value", [pytest.param("中's", id="apostrophe"), pytest.param("中\\path", id="backslash")])
|
||||
+def test_fish_quote_preserves_legacy_locale_bytes(value: str) -> None:
|
||||
+ version: Final = subprocess.check_output([FISH, "--version"], text=True, encoding="utf-8")
|
||||
+ if not version.startswith("fish, version 3."):
|
||||
+ pytest.skip("fish 4 always decodes UTF-8")
|
||||
+ locales: Final = subprocess.check_output(["locale", "-a"], text=True, encoding="utf-8").splitlines()
|
||||
+ if "zh_CN.gbk" not in locales:
|
||||
+ pytest.skip("GBK locale is unavailable")
|
||||
+ result: Final = subprocess.run(
|
||||
+ [FISH, "--no-config"],
|
||||
+ input=f"printf '%s' {FishActivator.quote(value)}\n".encode(),
|
||||
+ env={**os.environ, "LC_ALL": "zh_CN.gbk"},
|
||||
+ capture_output=True,
|
||||
+ timeout=30,
|
||||
+ check=True,
|
||||
+ )
|
||||
+ assert result.stdout == value.encode()
|
||||
+
|
||||
+
|
||||
+@pytest.mark.parametrize(
|
||||
+ ("value", "expected"),
|
||||
+ [
|
||||
+ pytest.param("café's", "'café'\\''s'", id="apostrophe"),
|
||||
+ pytest.param("café\\path", "'café'\\\\'path'", id="backslash"),
|
||||
+ ],
|
||||
+)
|
||||
+def test_fish_quote_separates_escapes(value: str, expected: str) -> None:
|
||||
+ assert FishActivator.quote(value) == expected
|
||||
+
|
||||
+
|
||||
@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:
|
||||
--
|
||||
2.35.6
|
||||
@@ -15,6 +15,8 @@ SRC_URI += " \
|
||||
file://CVE-2026-102930_p2.patch \
|
||||
file://CVE-2026-102930_p3.patch \
|
||||
file://CVE-2026-102925.patch \
|
||||
file://CVE-2026-113732_p1.patch \
|
||||
file://CVE-2026-113732_p2.patch \
|
||||
"
|
||||
|
||||
BBCLASSEXTEND = "native nativesdk"
|
||||
|
||||
Reference in new issue
Block a user