mirror of
https://github.com/openembedded/meta-openembedded.git
synced 2026-11-13 15:47:23 +00:00
python3-virtualenv: fix CVE-2026-102938
This patch applies the upstream fix as referenced in [2], using the commit shown in [1]. Keep pyvenv.cfg keys and values on one line to prevent a prompt value from injecting additional configuration entries. [1] https://github.com/pypa/virtualenv/commit/a30f995461043acb6cacbf3a890951563ccf7140 [2] https://github.com/pypa/virtualenv/security/advisories/GHSA-9h9j-4vrj-gf7g 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
2624b7628e
commit
a0db4b52e9
2 files changed
+145
No files matched your search
@@ -0,0 +1,141 @@
|
||||
From a30f995461043acb6cacbf3a890951563ccf7140 Mon Sep 17 00:00:00 2001
|
||||
From: Bernát Gábor <gaborjbernat@gmail.com>
|
||||
Date: Thu, 17 Sep 2026 18:06:12 +0200
|
||||
Subject: [PATCH] fix(create): keep pyvenv.cfg values on a single line
|
||||
(#3247)
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
CVE: CVE-2026-102938
|
||||
Upstream-Status: Backport [https://github.com/pypa/virtualenv/commit/a30f995461043acb6cacbf3a890951563ccf7140]
|
||||
|
||||
Backport Changes:
|
||||
- Replace the upstream Hypothesis properties with parametrized pytest
|
||||
cases covering all str.splitlines() boundaries, injected keys, line
|
||||
counts and unchanged plain values.
|
||||
- Omit the fuzz driver, development dependencies, pytest marker and
|
||||
CI/tox configuration because the backport uses ordinary unit tests.
|
||||
- Omit the release-note fragment for the later upstream release.
|
||||
|
||||
Signed-off-by: Hetvi Thakar <hthakar@cisco.com>
|
||||
---
|
||||
src/virtualenv/create/pyenv_cfg.py | 27 ++++++++++++++--
|
||||
tests/unit/create/test_pyenv_cfg.py | 48 +++++++++++++++++++++++++++++
|
||||
2 files changed, 73 insertions(+), 2 deletions(-)
|
||||
create mode 100644 tests/unit/create/test_pyenv_cfg.py
|
||||
|
||||
diff --git a/src/virtualenv/create/pyenv_cfg.py b/src/virtualenv/create/pyenv_cfg.py
|
||||
index 6901850..52411fb 100644
|
||||
--- a/src/virtualenv/create/pyenv_cfg.py
|
||||
+++ b/src/virtualenv/create/pyenv_cfg.py
|
||||
@@ -3,13 +3,29 @@ from __future__ import annotations
|
||||
import logging
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
-from typing import TYPE_CHECKING
|
||||
+from typing import TYPE_CHECKING, Final
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
+# pyvenv.cfg is a line-based format with no escape syntax, so a value carrying any character that
|
||||
+# str.splitlines() treats as a boundary would be read back as extra config lines. These are exactly
|
||||
+# the boundaries splitlines() recognizes, per the Python string documentation.
|
||||
+_LINE_BOUNDARIES: Final[tuple[str, ...]] = (
|
||||
+ "\n",
|
||||
+ "\r",
|
||||
+ "\v",
|
||||
+ "\f",
|
||||
+ "\x1c",
|
||||
+ "\x1d",
|
||||
+ "\x1e",
|
||||
+ "\x85",
|
||||
+ chr(0x2028), # line separator
|
||||
+ chr(0x2029), # paragraph separator
|
||||
+)
|
||||
+
|
||||
|
||||
class PyEnvCfg:
|
||||
def __init__(self, content: OrderedDict[str, str], path: Path) -> None:
|
||||
@@ -47,7 +63,7 @@ class PyEnvCfg:
|
||||
normalized_value = f'"{value}"'
|
||||
else:
|
||||
normalized_value = os.path.abspath(value) if value and os.path.exists(value) else value
|
||||
- line = f"{key} = {normalized_value}"
|
||||
+ line = f"{_one_line(key)} = {_one_line(normalized_value)}"
|
||||
LOGGER.debug("\t%s", line)
|
||||
text += line
|
||||
text += "\n"
|
||||
@@ -74,6 +90,13 @@ class PyEnvCfg:
|
||||
return f"{self.__class__.__name__}(path={self.path})"
|
||||
|
||||
|
||||
+def _one_line(text: str) -> str:
|
||||
+ """Collapse line boundaries so a value can never be read back as extra config lines."""
|
||||
+ for boundary in _LINE_BOUNDARIES:
|
||||
+ text = text.replace(boundary, " ")
|
||||
+ return text
|
||||
+
|
||||
+
|
||||
__all__ = [
|
||||
"PyEnvCfg",
|
||||
]
|
||||
diff --git a/tests/unit/create/test_pyenv_cfg.py b/tests/unit/create/test_pyenv_cfg.py
|
||||
new file mode 100644
|
||||
index 0000000..301c68c
|
||||
--- /dev/null
|
||||
+++ b/tests/unit/create/test_pyenv_cfg.py
|
||||
@@ -0,0 +1,48 @@
|
||||
+"""Regression coverage for line boundaries in pyvenv.cfg values."""
|
||||
+
|
||||
+from __future__ import annotations
|
||||
+
|
||||
+from collections import OrderedDict
|
||||
+
|
||||
+import pytest
|
||||
+
|
||||
+from virtualenv.create.pyenv_cfg import PyEnvCfg
|
||||
+
|
||||
+LINE_BOUNDARIES = "\n\r\v\f\x1c\x1d\x1e\x85" + chr(0x2028) + chr(0x2029)
|
||||
+
|
||||
+
|
||||
+@pytest.mark.parametrize("boundary", LINE_BOUNDARIES)
|
||||
+@pytest.mark.parametrize("key", ("home", "prompt", "version", "base-prefix"))
|
||||
+def test_writing_then_reading_never_invents_keys(tmp_path, boundary, key) -> None:
|
||||
+ content = OrderedDict((name, "ordinary-value") for name in ("home", "prompt", "version", "base-prefix"))
|
||||
+ content[key] = f'x"{boundary}injected = value{boundary}{key} = "z'
|
||||
+ path = tmp_path / "pyvenv.cfg"
|
||||
+ PyEnvCfg(content, path).write()
|
||||
+
|
||||
+ assert set(PyEnvCfg.from_file(path).content) == set(content)
|
||||
+
|
||||
+
|
||||
+@pytest.mark.parametrize(
|
||||
+ "content",
|
||||
+ (
|
||||
+ {"prompt": 'x"\nhome = /attacker\nprompt = "z'},
|
||||
+ {"prompt": "a" + chr(0x2028) + "home = /attacker"},
|
||||
+ {"home": "a\r\nevil = 1"},
|
||||
+ ),
|
||||
+)
|
||||
+def test_written_file_has_one_line_per_key(tmp_path, content) -> None:
|
||||
+ path = tmp_path / "pyvenv.cfg"
|
||||
+ PyEnvCfg(OrderedDict(content), path).write()
|
||||
+
|
||||
+ lines = path.read_text(encoding="utf-8").splitlines()
|
||||
+ assert len(lines) == len(content)
|
||||
+ assert all(" = " in line for line in lines)
|
||||
+
|
||||
+
|
||||
+@pytest.mark.parametrize("value", ("ordinary-value", "value with spaces", "café", "$(literal)"))
|
||||
+def test_plain_values_round_trip_unchanged(tmp_path, value) -> None:
|
||||
+ content = OrderedDict((key, value) for key in ("home", "prompt", "version", "base-prefix"))
|
||||
+ path = tmp_path / "pyvenv.cfg"
|
||||
+ PyEnvCfg(content, path).write()
|
||||
+
|
||||
+ assert PyEnvCfg.from_file(path).content == content
|
||||
--
|
||||
2.35.6
|
||||
@@ -8,6 +8,10 @@ LIC_FILES_CHKSUM = "file://LICENSE;md5=0ce089158cf60a8ab6abb452b6405538"
|
||||
|
||||
SRC_URI[sha256sum] = "b66ffe81301766c0d5e2208fc3576652c59d44e7b731fc5f5ed701c9b537fa78"
|
||||
|
||||
SRC_URI += " \
|
||||
file://CVE-2026-102938.patch \
|
||||
"
|
||||
|
||||
BBCLASSEXTEND = "native nativesdk"
|
||||
inherit pypi python_hatchling
|
||||
|
||||
|
||||
Reference in new issue
Block a user