Skip to content

Commit ef6788f

Browse files
authored
Merge pull request #71 from ActiveState/BE-5264-cve-2024-9287-python-3-7
Be-5264 CVE 2024 9287 python 3 7
2 parents b692998 + 9b114d5 commit ef6788f

File tree

7 files changed

+131
-51
lines changed

7 files changed

+131
-51
lines changed

Lib/test/test_venv.py

+81
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import subprocess
1515
import sys
1616
import tempfile
17+
import shlex
1718
from test.support import (captured_stdout, captured_stderr, requires_zlib,
1819
can_symlink, EnvironmentVarGuard, rmtree)
1920
import threading
@@ -83,6 +84,10 @@ def get_text_file_contents(self, *args):
8384
result = f.read()
8485
return result
8586

87+
def assertEndsWith(self, string, tail):
88+
if not string.endswith(tail):
89+
self.fail(f"String {string!r} does not end with {tail!r}")
90+
8691
class BasicTest(BaseTest):
8792
"""Test venv module functionality."""
8893

@@ -293,6 +298,82 @@ def test_executable_symlinks(self):
293298
'import sys; print(sys.executable)'])
294299
self.assertEqual(out.strip(), envpy.encode())
295300

301+
# gh-124651: test quoted strings
302+
@unittest.skipIf(os.name == 'nt', 'contains invalid characters on Windows')
303+
def test_special_chars_bash(self):
304+
"""
305+
Test that the template strings are quoted properly (bash)
306+
"""
307+
rmtree(self.env_dir)
308+
bash = shutil.which('bash')
309+
if bash is None:
310+
self.skipTest('bash required for this test')
311+
env_name = '"\';&&$e|\'"'
312+
env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
313+
builder = venv.EnvBuilder(clear=True)
314+
builder.create(env_dir)
315+
activate = os.path.join(env_dir, self.bindir, 'activate')
316+
test_script = os.path.join(self.env_dir, 'test_special_chars.sh')
317+
with open(test_script, "w") as f:
318+
f.write(f'source {shlex.quote(activate)}\n'
319+
'python -c \'import sys; print(sys.executable)\'\n'
320+
'python -c \'import os; print(os.environ["VIRTUAL_ENV"])\'\n'
321+
'deactivate\n')
322+
out, err = check_output([bash, test_script])
323+
lines = out.splitlines()
324+
self.assertTrue(env_name.encode() in lines[0])
325+
self.assertEndsWith(lines[1], env_name.encode())
326+
327+
# gh-124651: test quoted strings
328+
@unittest.skipIf(os.name == 'nt', 'contains invalid characters on Windows')
329+
def test_special_chars_csh(self):
330+
"""
331+
Test that the template strings are quoted properly (csh)
332+
"""
333+
rmtree(self.env_dir)
334+
csh = shutil.which('tcsh') or shutil.which('csh')
335+
if csh is None:
336+
self.skipTest('csh required for this test')
337+
env_name = '"\';&&$e|\'"'
338+
env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
339+
builder = venv.EnvBuilder(clear=True)
340+
builder.create(env_dir)
341+
activate = os.path.join(env_dir, self.bindir, 'activate.csh')
342+
test_script = os.path.join(self.env_dir, 'test_special_chars.csh')
343+
with open(test_script, "w") as f:
344+
f.write(f'source {shlex.quote(activate)}\n'
345+
'python -c \'import sys; print(sys.executable)\'\n'
346+
'python -c \'import os; print(os.environ["VIRTUAL_ENV"])\'\n'
347+
'deactivate\n')
348+
out, err = check_output([csh, test_script])
349+
lines = out.splitlines()
350+
self.assertTrue(env_name.encode() in lines[0])
351+
self.assertEndsWith(lines[1], env_name.encode())
352+
353+
# gh-124651: test quoted strings on Windows
354+
@unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')
355+
def test_special_chars_windows(self):
356+
"""
357+
Test that the template strings are quoted properly on Windows
358+
"""
359+
rmtree(self.env_dir)
360+
env_name = "'&&^$e"
361+
env_dir = os.path.join(os.path.realpath(self.env_dir), env_name)
362+
builder = venv.EnvBuilder(clear=True)
363+
builder.create(env_dir)
364+
activate = os.path.join(env_dir, self.bindir, 'activate.bat')
365+
test_batch = os.path.join(self.env_dir, 'test_special_chars.bat')
366+
with open(test_batch, "w") as f:
367+
f.write('@echo off\n'
368+
f'"{activate}" & '
369+
f'{self.exe} -c "import sys; print(sys.executable)" & '
370+
f'{self.exe} -c "import os; print(os.environ[\'VIRTUAL_ENV\'])" & '
371+
'deactivate')
372+
out, err = check_output([test_batch])
373+
lines = out.splitlines()
374+
self.assertTrue(env_name.encode() in lines[0])
375+
self.assertEndsWith(lines[1], env_name.encode())
376+
296377
@unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')
297378
def test_unicode_in_batch_file(self):
298379
"""

Lib/venv/__init__.py

+37-5
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import sys
1212
import sysconfig
1313
import types
14+
import shlex
1415

1516
logger = logging.getLogger(__name__)
1617

@@ -323,11 +324,41 @@ def replace_variables(self, text, context):
323324
:param context: The information for the environment creation request
324325
being processed.
325326
"""
326-
text = text.replace('__VENV_DIR__', context.env_dir)
327-
text = text.replace('__VENV_NAME__', context.env_name)
328-
text = text.replace('__VENV_PROMPT__', context.prompt)
329-
text = text.replace('__VENV_BIN_NAME__', context.bin_name)
330-
text = text.replace('__VENV_PYTHON__', context.env_exe)
327+
replacements = {
328+
'__VENV_DIR__': context.env_dir,
329+
'__VENV_NAME__': context.env_name,
330+
'__VENV_PROMPT__': context.prompt,
331+
'__VENV_BIN_NAME__': context.bin_name,
332+
'__VENV_PYTHON__': context.env_exe,
333+
}
334+
335+
def quote_ps1(s):
336+
"""
337+
This should satisfy PowerShell quoting rules [1], unless the quoted
338+
string is passed directly to Windows native commands [2].
339+
[1]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules
340+
[2]: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_parsing#passing-arguments-that-contain-quote-characters
341+
"""
342+
s = s.replace("'", "''")
343+
return f"'{s}'"
344+
345+
def quote_bat(s):
346+
return s
347+
348+
# gh-124651: need to quote the template strings properly
349+
quote = shlex.quote
350+
script_path = context.script_path
351+
if script_path.endswith('.ps1'):
352+
quote = quote_ps1
353+
elif script_path.endswith('.bat'):
354+
quote = quote_bat
355+
else:
356+
# fallbacks to POSIX shell compliant quote
357+
quote = shlex.quote
358+
359+
replacements = {key: quote(s) for key, s in replacements.items()}
360+
for key, quoted in replacements.items():
361+
text = text.replace(key, quoted)
331362
return text
332363

333364
def install_scripts(self, context, path):
@@ -367,6 +398,7 @@ def install_scripts(self, context, path):
367398
with open(srcfile, 'rb') as f:
368399
data = f.read()
369400
if not srcfile.endswith(('.exe', '.pdb')):
401+
context.script_path = srcfile
370402
try:
371403
data = data.decode('utf-8')
372404
data = self.replace_variables(data, context)

Lib/venv/scripts/common/activate

+3-13
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,11 @@ deactivate () {
3737
# unset irrelevant variables
3838
deactivate nondestructive
3939

40-
VIRTUAL_ENV="__VENV_DIR__"
40+
VIRTUAL_ENV=__VENV_DIR__
4141
export VIRTUAL_ENV
4242

4343
_OLD_VIRTUAL_PATH="$PATH"
44-
PATH="$VIRTUAL_ENV/__VENV_BIN_NAME__:$PATH"
44+
PATH="$VIRTUAL_ENV/"__VENV_BIN_NAME__":$PATH"
4545
export PATH
4646

4747
# unset PYTHONHOME if set
@@ -54,17 +54,7 @@ fi
5454

5555
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
5656
_OLD_VIRTUAL_PS1="${PS1:-}"
57-
if [ "x__VENV_PROMPT__" != x ] ; then
58-
PS1="__VENV_PROMPT__${PS1:-}"
59-
else
60-
if [ "`basename \"$VIRTUAL_ENV\"`" = "__" ] ; then
61-
# special case for Aspen magic directories
62-
# see http://www.zetadev.com/software/aspen/
63-
PS1="[`basename \`dirname \"$VIRTUAL_ENV\"\``] $PS1"
64-
else
65-
PS1="(`basename \"$VIRTUAL_ENV\"`)$PS1"
66-
fi
67-
fi
57+
PS1=__VENV_PROMPT__"${PS1:-}"
6858
export PS1
6959
fi
7060

Lib/venv/scripts/nt/activate.bat

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ if defined _OLD_CODEPAGE (
88
"%SystemRoot%\System32\chcp.com" 65001 > nul
99
)
1010

11-
set VIRTUAL_ENV=__VENV_DIR__
11+
set "VIRTUAL_ENV=__VENV_DIR__"
1212

1313
if not defined PROMPT set PROMPT=$P$G
1414

@@ -24,7 +24,7 @@ set PYTHONHOME=
2424
if defined _OLD_VIRTUAL_PATH set PATH=%_OLD_VIRTUAL_PATH%
2525
if not defined _OLD_VIRTUAL_PATH set _OLD_VIRTUAL_PATH=%PATH%
2626

27-
set PATH=%VIRTUAL_ENV%\__VENV_BIN_NAME__;%PATH%
27+
set "PATH=%VIRTUAL_ENV%\__VENV_BIN_NAME__;%PATH%"
2828

2929
:END
3030
if defined _OLD_CODEPAGE (

Lib/venv/scripts/posix/activate.csh

+3-15
Original file line numberDiff line numberDiff line change
@@ -8,28 +8,16 @@ alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PA
88
# Unset irrelevant variables.
99
deactivate nondestructive
1010

11-
setenv VIRTUAL_ENV "__VENV_DIR__"
11+
setenv VIRTUAL_ENV __VENV_DIR__
1212

1313
set _OLD_VIRTUAL_PATH="$PATH"
14-
setenv PATH "$VIRTUAL_ENV/__VENV_BIN_NAME__:$PATH"
14+
setenv PATH "$VIRTUAL_ENV/"__VENV_BIN_NAME__":$PATH"
1515

1616

1717
set _OLD_VIRTUAL_PROMPT="$prompt"
1818

1919
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
20-
if ("__VENV_NAME__" != "") then
21-
set env_name = "__VENV_NAME__"
22-
else
23-
if (`basename "VIRTUAL_ENV"` == "__") then
24-
# special case for Aspen magic directories
25-
# see http://www.zetadev.com/software/aspen/
26-
set env_name = `basename \`dirname "$VIRTUAL_ENV"\``
27-
else
28-
set env_name = `basename "$VIRTUAL_ENV"`
29-
endif
30-
endif
31-
set prompt = "[$env_name] $prompt"
32-
unset env_name
20+
set prompt = __VENV_PROMPT__"$prompt"
3321
endif
3422

3523
alias pydoc python -m pydoc

Lib/venv/scripts/posix/activate.fish

+4-16
Original file line numberDiff line numberDiff line change
@@ -29,10 +29,10 @@ end
2929
# unset irrelevant variables
3030
deactivate nondestructive
3131

32-
set -gx VIRTUAL_ENV "__VENV_DIR__"
32+
set -gx VIRTUAL_ENV __VENV_DIR__
3333

3434
set -gx _OLD_VIRTUAL_PATH $PATH
35-
set -gx PATH "$VIRTUAL_ENV/__VENV_BIN_NAME__" $PATH
35+
set -gx PATH "$VIRTUAL_ENV/"__VENV_BIN_NAME__ $PATH
3636

3737
# unset PYTHONHOME if set
3838
if set -q PYTHONHOME
@@ -51,20 +51,8 @@ if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
5151
# Save the return status of the last command
5252
set -l old_status $status
5353

54-
# Prompt override?
55-
if test -n "__VENV_PROMPT__"
56-
printf "%s%s" "__VENV_PROMPT__" (set_color normal)
57-
else
58-
# ...Otherwise, prepend env
59-
set -l _checkbase (basename "$VIRTUAL_ENV")
60-
if test $_checkbase = "__"
61-
# special case for Aspen magic directories
62-
# see http://www.zetadev.com/software/aspen/
63-
printf "%s[%s]%s " (set_color -b blue white) (basename (dirname "$VIRTUAL_ENV")) (set_color normal)
64-
else
65-
printf "%s(%s)%s" (set_color -b blue white) (basename "$VIRTUAL_ENV") (set_color normal)
66-
end
67-
end
54+
# Output the venv prompt; color taken from the blue of the Python logo.
55+
printf "%s%s%s" (set_color 4B8BBE) __VENV_PROMPT__ (set_color normal)
6856

6957
# Restore the return status of the previous command.
7058
echo "exit $old_status" | .
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Properly quote template strings in :mod:`venv` activation scripts.

0 commit comments

Comments
 (0)