File CVE-2024-9287-venv_path_unquoted.patch of Package python3.36643
From ae0d64cb185900712c40a65d7d8aa118f9903d57 Mon Sep 17 00:00:00 2001
From: Victor Stinner <vstinner@python.org>
Date: Fri, 1 Nov 2024 14:11:47 +0100
Subject: [PATCH] [3.11] gh-124651: Quote template strings in `venv` activation
scripts (GH-124712) (GH-126185) (#126269)
(cherry picked from commit ae961ae94bf19c8f8c7fbea3d1c25cc55ce8ae97)
---
Lib/test/test_venv.py | 82 ++++++++++
Lib/venv/__init__.py | 42 ++++-
Lib/venv/scripts/posix/activate | 6
Lib/venv/scripts/posix/activate.csh | 6
Lib/venv/scripts/posix/activate.fish | 6
Misc/NEWS.d/next/Library/2024-09-28-02-03-04.gh-issue-124651.bLBGtH.rst | 1
6 files changed, 129 insertions(+), 14 deletions(-)
create mode 100644 Misc/NEWS.d/next/Library/2024-09-28-02-03-04.gh-issue-124651.bLBGtH.rst
--- a/Lib/test/test_venv.py
+++ b/Lib/test/test_venv.py
@@ -8,6 +8,8 @@ Licensed to the PSF under a contributor
import ensurepip
import os
import os.path
+import shlex
+import shutil
import struct
import subprocess
import sys
@@ -71,6 +73,10 @@ class BaseTest(unittest.TestCase):
result = f.read()
return result
+ def assertEndsWith(self, string, tail):
+ if not string.endswith(tail):
+ self.fail("String {!r} does not end with {!r}".format(string, tail))
+
class BasicTest(BaseTest):
"""Test venv module functionality."""
@@ -277,6 +283,82 @@ class BasicTest(BaseTest):
out, err = p.communicate()
self.assertEqual(out.strip(), envpy.encode())
+ # gh-124651: test quoted strings
+ @unittest.skipIf(os.name == 'nt', 'contains invalid characters on Windows')
+ def test_special_chars_bash(self):
+ """
+ Test that the template strings are quoted properly (bash)
+ """
+ rmtree(self.env_dir)
+ bash = shutil.which('bash')
+ if bash is None:
+ self.skipTest('bash required for this test')
+ env_name = '"\';&&$e|\'"'
+ env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
+ builder = venv.EnvBuilder(clear=True)
+ builder.create(env_dir)
+ activate = os.path.join(env_dir, self.bindir, 'activate')
+ test_script = os.path.join(self.env_dir, 'test_special_chars.sh')
+ with open(test_script, "w") as f:
+ f.write('source {}\n'.format(shlex.quote(activate)) +
+ 'python -c \'import sys; print(sys.executable)\'\n' +
+ 'python -c \'import os; print(os.environ["VIRTUAL_ENV"])\'\n' +
+ 'deactivate\n')
+ out = subprocess.check_output([bash, test_script])
+ lines = out.splitlines()
+ self.assertTrue(env_name.encode() in lines[0])
+ self.assertEndsWith(lines[1], env_name.encode())
+
+ # gh-124651: test quoted strings
+ @unittest.skipIf(os.name == 'nt', 'contains invalid characters on Windows')
+ def test_special_chars_csh(self):
+ """
+ Test that the template strings are quoted properly (csh)
+ """
+ rmtree(self.env_dir)
+ csh = shutil.which('tcsh') or shutil.which('csh')
+ if csh is None:
+ self.skipTest('csh required for this test')
+ env_name = '"\';&&$e|\'"'
+ env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
+ builder = venv.EnvBuilder(clear=True)
+ builder.create(env_dir)
+ activate = os.path.join(env_dir, self.bindir, 'activate.csh')
+ test_script = os.path.join(self.env_dir, 'test_special_chars.csh')
+ with open(test_script, "w") as f:
+ f.write('source {}\n'.format(shlex.quote(activate)) +
+ 'python -c \'import sys; print(sys.executable)\'\n' +
+ 'python -c \'import os; print(os.environ["VIRTUAL_ENV"])\'\n' +
+ 'deactivate\n')
+ out = subprocess.check_output([csh, test_script])
+ lines = out.splitlines()
+ self.assertTrue(env_name.encode() in lines[0])
+ self.assertEndsWith(lines[1], env_name.encode())
+
+ # gh-124651: test quoted strings on Windows
+ @unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')
+ def test_special_chars_windows(self):
+ """
+ Test that the template strings are quoted properly on Windows
+ """
+ rmtree(self.env_dir)
+ env_name = "'&&^$e"
+ env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
+ builder = venv.EnvBuilder(clear=True)
+ builder.create(env_dir)
+ activate = os.path.join(env_dir, self.bindir, 'activate.bat')
+ test_batch = os.path.join(self.env_dir, 'test_special_chars.bat')
+ with open(test_batch, "w") as f:
+ f.write('@echo off\n'
+ '"{}" & '.format(activate) +
+ '{} -c "import sys; print(sys.executable)" & '.format(self.exe) +
+ '{} -c "import os; print(os.environ[\'VIRTUAL_ENV\'])" & '.format(self.exe) +
+ 'deactivate')
+ out = subprocess.check_output([test_batch])
+ lines = out.splitlines()
+ self.assertTrue(env_name.encode() in lines[0])
+ self.assertEndsWith(lines[1], env_name.encode())
+
@skipInVenv
class EnsurePipTest(BaseTest):
--- a/Lib/venv/__init__.py
+++ b/Lib/venv/__init__.py
@@ -33,6 +33,7 @@ import shutil
import subprocess
import sys
import types
+import shlex
logger = logging.getLogger(__name__)
@@ -292,11 +293,41 @@ class EnvBuilder:
:param context: The information for the environment creation request
being processed.
"""
- text = text.replace('__VENV_DIR__', context.env_dir)
- text = text.replace('__VENV_NAME__', context.env_name)
- text = text.replace('__VENV_PROMPT__', context.prompt)
- text = text.replace('__VENV_BIN_NAME__', context.bin_name)
- text = text.replace('__VENV_PYTHON__', context.env_exe)
+ replacements = {
+ '__VENV_DIR__': context.env_dir,
+ '__VENV_NAME__': context.env_name,
+ '__VENV_PROMPT__': context.prompt,
+ '__VENV_BIN_NAME__': context.bin_name,
+ '__VENV_PYTHON__': context.env_exe,
+ }
+
+ def quote_ps1(s):
+ """
+ This should satisfy PowerShell quoting rules [1], unless the quoted
+ string is passed directly to Windows native commands [2].
+ [1]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules
+ [2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parsing#passing-arguments-that-contain-quote-characters
+ """
+ s = s.replace("'", "''")
+ return "'{}'".format(s)
+
+ def quote_bat(s):
+ return s
+
+ # gh-124651: need to quote the template strings properly
+ quote = shlex.quote
+ script_path = context.script_path
+ if script_path.endswith('.ps1'):
+ quote = quote_ps1
+ elif script_path.endswith('.bat'):
+ quote = quote_bat
+ else:
+ # fallbacks to POSIX shell compliant quote
+ quote = shlex.quote
+
+ replacements = {key: quote(s) for key, s in replacements.items()}
+ for key, quoted in replacements.items():
+ text = text.replace(key, quoted)
return text
def install_scripts(self, context, path):
@@ -336,6 +367,7 @@ class EnvBuilder:
mode = 'wb'
else:
mode = 'w'
+ context.script_path = srcfile
try:
data = data.decode('utf-8')
data = self.replace_variables(data, context)
--- a/Lib/venv/scripts/posix/activate
+++ b/Lib/venv/scripts/posix/activate
@@ -37,11 +37,11 @@ deactivate () {
# unset irrelavent variables
deactivate nondestructive
-VIRTUAL_ENV="__VENV_DIR__"
+VIRTUAL_ENV=__VENV_DIR__
export VIRTUAL_ENV
_OLD_VIRTUAL_PATH="$PATH"
-PATH="$VIRTUAL_ENV/__VENV_BIN_NAME__:$PATH"
+PATH="$VIRTUAL_ENV/"__VENV_BIN_NAME__":$PATH"
export PATH
# unset PYTHONHOME if set
@@ -55,7 +55,7 @@ fi
if [ -z "$VIRTUAL_ENV_DISABLE_PROMPT" ] ; then
_OLD_VIRTUAL_PS1="$PS1"
if [ "x__VENV_PROMPT__" != x ] ; then
- PS1="__VENV_PROMPT__$PS1"
+ PS1="__VENV_PROMPT__"${PS1:-}"
else
if [ "`basename \"$VIRTUAL_ENV\"`" = "__" ] ; then
# special case for Aspen magic directories
--- a/Lib/venv/scripts/posix/activate.csh
+++ b/Lib/venv/scripts/posix/activate.csh
@@ -8,17 +8,17 @@ alias deactivate 'test $?_OLD_VIRTUAL_PA
# Unset irrelavent variables.
deactivate nondestructive
-setenv VIRTUAL_ENV "__VENV_DIR__"
+setenv VIRTUAL_ENV __VENV_DIR__
set _OLD_VIRTUAL_PATH="$PATH"
-setenv PATH "$VIRTUAL_ENV/__VENV_BIN_NAME__:$PATH"
+setenv PATH "$VIRTUAL_ENV/"__VENV_BIN_NAME__":$PATH"
set _OLD_VIRTUAL_PROMPT="$prompt"
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
if ("__VENV_NAME__" != "") then
- set env_name = "__VENV_NAME__"
+ set env_name = __VENV_NAME__
else
if (`basename "VIRTUAL_ENV"` == "__") then
# special case for Aspen magic directories
--- a/Lib/venv/scripts/posix/activate.fish
+++ b/Lib/venv/scripts/posix/activate.fish
@@ -32,10 +32,10 @@ end
# unset irrelavent variables
deactivate nondestructive
-set -gx VIRTUAL_ENV "__VENV_DIR__"
+set -gx VIRTUAL_ENV __VENV_DIR__
set -gx _OLD_VIRTUAL_PATH $PATH
-set -gx PATH "$VIRTUAL_ENV/__VENV_BIN_NAME__" $PATH
+set -gx PATH "$VIRTUAL_ENV/"__VENV_BIN_NAME__ $PATH
# unset PYTHONHOME if set
if set -q PYTHONHOME
@@ -56,7 +56,7 @@ if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
function fish_prompt
# Prompt override?
if test -n "__VENV_PROMPT__"
- printf "%s%s%s" "__VENV_PROMPT__" (set_color normal) (_old_fish_prompt)
+ printf "%s%s%s" __VENV_PROMPT__ (set_color normal) (_old_fish_prompt)
return
end
# ...Otherwise, prepend env
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2024-09-28-02-03-04.gh-issue-124651.bLBGtH.rst
@@ -0,0 +1 @@
+Properly quote template strings in :mod:`venv` activation scripts.