mirror of
https://github.com/python/cpython.git
synced 2025-08-23 02:04:56 +00:00
gh-124651: Quote template strings in venv
activation scripts (GH-124712)
This patch properly quotes template strings in `venv` activation scripts. This mitigates potential command injection.
This commit is contained in:
parent
44f841f01a
commit
d48cc82ed2
7 changed files with 135 additions and 21 deletions
|
@ -17,6 +17,7 @@ import subprocess
|
||||||
import sys
|
import sys
|
||||||
import sysconfig
|
import sysconfig
|
||||||
import tempfile
|
import tempfile
|
||||||
|
import shlex
|
||||||
from test.support import (captured_stdout, captured_stderr,
|
from test.support import (captured_stdout, captured_stderr,
|
||||||
skip_if_broken_multiprocessing_synchronize, verbose,
|
skip_if_broken_multiprocessing_synchronize, verbose,
|
||||||
requires_subprocess, is_android, is_apple_mobile,
|
requires_subprocess, is_android, is_apple_mobile,
|
||||||
|
@ -110,6 +111,10 @@ class BaseTest(unittest.TestCase):
|
||||||
result = f.read()
|
result = f.read()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def assertEndsWith(self, string, tail):
|
||||||
|
if not string.endswith(tail):
|
||||||
|
self.fail(f"String {string!r} does not end with {tail!r}")
|
||||||
|
|
||||||
class BasicTest(BaseTest):
|
class BasicTest(BaseTest):
|
||||||
"""Test venv module functionality."""
|
"""Test venv module functionality."""
|
||||||
|
|
||||||
|
@ -488,6 +493,82 @@ class BasicTest(BaseTest):
|
||||||
'import sys; print(sys.executable)'])
|
'import sys; print(sys.executable)'])
|
||||||
self.assertEqual(out.strip(), envpy.encode())
|
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(f'source {shlex.quote(activate)}\n'
|
||||||
|
'python -c \'import sys; print(sys.executable)\'\n'
|
||||||
|
'python -c \'import os; print(os.environ["VIRTUAL_ENV"])\'\n'
|
||||||
|
'deactivate\n')
|
||||||
|
out, err = 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(f'source {shlex.quote(activate)}\n'
|
||||||
|
'python -c \'import sys; print(sys.executable)\'\n'
|
||||||
|
'python -c \'import os; print(os.environ["VIRTUAL_ENV"])\'\n'
|
||||||
|
'deactivate\n')
|
||||||
|
out, err = 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'
|
||||||
|
f'"{activate}" & '
|
||||||
|
f'{self.exe} -c "import sys; print(sys.executable)" & '
|
||||||
|
f'{self.exe} -c "import os; print(os.environ[\'VIRTUAL_ENV\'])" & '
|
||||||
|
'deactivate')
|
||||||
|
out, err = check_output([test_batch])
|
||||||
|
lines = out.splitlines()
|
||||||
|
self.assertTrue(env_name.encode() in lines[0])
|
||||||
|
self.assertEndsWith(lines[1], env_name.encode())
|
||||||
|
|
||||||
@unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')
|
@unittest.skipUnless(os.name == 'nt', 'only relevant on Windows')
|
||||||
def test_unicode_in_batch_file(self):
|
def test_unicode_in_batch_file(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -11,6 +11,7 @@ import subprocess
|
||||||
import sys
|
import sys
|
||||||
import sysconfig
|
import sysconfig
|
||||||
import types
|
import types
|
||||||
|
import shlex
|
||||||
|
|
||||||
|
|
||||||
CORE_VENV_DEPS = ('pip',)
|
CORE_VENV_DEPS = ('pip',)
|
||||||
|
@ -484,11 +485,41 @@ class EnvBuilder:
|
||||||
:param context: The information for the environment creation request
|
:param context: The information for the environment creation request
|
||||||
being processed.
|
being processed.
|
||||||
"""
|
"""
|
||||||
text = text.replace('__VENV_DIR__', context.env_dir)
|
replacements = {
|
||||||
text = text.replace('__VENV_NAME__', context.env_name)
|
'__VENV_DIR__': context.env_dir,
|
||||||
text = text.replace('__VENV_PROMPT__', context.prompt)
|
'__VENV_NAME__': context.env_name,
|
||||||
text = text.replace('__VENV_BIN_NAME__', context.bin_name)
|
'__VENV_PROMPT__': context.prompt,
|
||||||
text = text.replace('__VENV_PYTHON__', context.env_exe)
|
'__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 f"'{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
|
return text
|
||||||
|
|
||||||
def install_scripts(self, context, path):
|
def install_scripts(self, context, path):
|
||||||
|
@ -538,6 +569,7 @@ class EnvBuilder:
|
||||||
with open(srcfile, 'rb') as f:
|
with open(srcfile, 'rb') as f:
|
||||||
data = f.read()
|
data = f.read()
|
||||||
try:
|
try:
|
||||||
|
context.script_path = srcfile
|
||||||
new_data = (
|
new_data = (
|
||||||
self.replace_variables(data.decode('utf-8'), context)
|
self.replace_variables(data.decode('utf-8'), context)
|
||||||
.encode('utf-8')
|
.encode('utf-8')
|
||||||
|
|
|
@ -41,20 +41,20 @@ case "$(uname)" in
|
||||||
CYGWIN*|MSYS*|MINGW*)
|
CYGWIN*|MSYS*|MINGW*)
|
||||||
# transform D:\path\to\venv to /d/path/to/venv on MSYS and MINGW
|
# transform D:\path\to\venv to /d/path/to/venv on MSYS and MINGW
|
||||||
# and to /cygdrive/d/path/to/venv on Cygwin
|
# and to /cygdrive/d/path/to/venv on Cygwin
|
||||||
VIRTUAL_ENV=$(cygpath "__VENV_DIR__")
|
VIRTUAL_ENV=$(cygpath __VENV_DIR__)
|
||||||
export VIRTUAL_ENV
|
export VIRTUAL_ENV
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
# use the path as-is
|
# use the path as-is
|
||||||
export VIRTUAL_ENV="__VENV_DIR__"
|
export VIRTUAL_ENV=__VENV_DIR__
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
_OLD_VIRTUAL_PATH="$PATH"
|
_OLD_VIRTUAL_PATH="$PATH"
|
||||||
PATH="$VIRTUAL_ENV/__VENV_BIN_NAME__:$PATH"
|
PATH="$VIRTUAL_ENV/"__VENV_BIN_NAME__":$PATH"
|
||||||
export PATH
|
export PATH
|
||||||
|
|
||||||
VIRTUAL_ENV_PROMPT="__VENV_PROMPT__"
|
VIRTUAL_ENV_PROMPT=__VENV_PROMPT__
|
||||||
export VIRTUAL_ENV_PROMPT
|
export VIRTUAL_ENV_PROMPT
|
||||||
|
|
||||||
# unset PYTHONHOME if set
|
# unset PYTHONHOME if set
|
||||||
|
@ -67,7 +67,7 @@ fi
|
||||||
|
|
||||||
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then
|
||||||
_OLD_VIRTUAL_PS1="${PS1:-}"
|
_OLD_VIRTUAL_PS1="${PS1:-}"
|
||||||
PS1="(__VENV_PROMPT__) ${PS1:-}"
|
PS1="("__VENV_PROMPT__") ${PS1:-}"
|
||||||
export PS1
|
export PS1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
|
@ -33,11 +33,11 @@ end
|
||||||
# Unset irrelevant variables.
|
# Unset irrelevant variables.
|
||||||
deactivate nondestructive
|
deactivate nondestructive
|
||||||
|
|
||||||
set -gx VIRTUAL_ENV "__VENV_DIR__"
|
set -gx VIRTUAL_ENV __VENV_DIR__
|
||||||
|
|
||||||
set -gx _OLD_VIRTUAL_PATH $PATH
|
set -gx _OLD_VIRTUAL_PATH $PATH
|
||||||
set -gx PATH "$VIRTUAL_ENV/__VENV_BIN_NAME__" $PATH
|
set -gx PATH "$VIRTUAL_ENV/"__VENV_BIN_NAME__ $PATH
|
||||||
set -gx VIRTUAL_ENV_PROMPT "__VENV_PROMPT__"
|
set -gx VIRTUAL_ENV_PROMPT __VENV_PROMPT__
|
||||||
|
|
||||||
# Unset PYTHONHOME if set.
|
# Unset PYTHONHOME if set.
|
||||||
if set -q PYTHONHOME
|
if set -q PYTHONHOME
|
||||||
|
@ -57,7 +57,7 @@ if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
|
||||||
set -l old_status $status
|
set -l old_status $status
|
||||||
|
|
||||||
# Output the venv prompt; color taken from the blue of the Python logo.
|
# Output the venv prompt; color taken from the blue of the Python logo.
|
||||||
printf "%s(%s)%s " (set_color 4B8BBE) "__VENV_PROMPT__" (set_color normal)
|
printf "%s(%s)%s " (set_color 4B8BBE) __VENV_PROMPT__ (set_color normal)
|
||||||
|
|
||||||
# Restore the return status of the previous command.
|
# Restore the return status of the previous command.
|
||||||
echo "exit $old_status" | .
|
echo "exit $old_status" | .
|
||||||
|
|
|
@ -8,7 +8,7 @@ if defined _OLD_CODEPAGE (
|
||||||
"%SystemRoot%\System32\chcp.com" 65001 > nul
|
"%SystemRoot%\System32\chcp.com" 65001 > nul
|
||||||
)
|
)
|
||||||
|
|
||||||
set VIRTUAL_ENV=__VENV_DIR__
|
set "VIRTUAL_ENV=__VENV_DIR__"
|
||||||
|
|
||||||
if not defined PROMPT set PROMPT=$P$G
|
if not defined PROMPT set PROMPT=$P$G
|
||||||
|
|
||||||
|
@ -24,8 +24,8 @@ set PYTHONHOME=
|
||||||
if defined _OLD_VIRTUAL_PATH set PATH=%_OLD_VIRTUAL_PATH%
|
if defined _OLD_VIRTUAL_PATH set PATH=%_OLD_VIRTUAL_PATH%
|
||||||
if not defined _OLD_VIRTUAL_PATH set _OLD_VIRTUAL_PATH=%PATH%
|
if not defined _OLD_VIRTUAL_PATH set _OLD_VIRTUAL_PATH=%PATH%
|
||||||
|
|
||||||
set PATH=%VIRTUAL_ENV%\__VENV_BIN_NAME__;%PATH%
|
set "PATH=%VIRTUAL_ENV%\__VENV_BIN_NAME__;%PATH%"
|
||||||
set VIRTUAL_ENV_PROMPT=__VENV_PROMPT__
|
set "VIRTUAL_ENV_PROMPT=__VENV_PROMPT__"
|
||||||
|
|
||||||
:END
|
:END
|
||||||
if defined _OLD_CODEPAGE (
|
if defined _OLD_CODEPAGE (
|
||||||
|
|
|
@ -9,17 +9,17 @@ alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PA
|
||||||
# Unset irrelevant variables.
|
# Unset irrelevant variables.
|
||||||
deactivate nondestructive
|
deactivate nondestructive
|
||||||
|
|
||||||
setenv VIRTUAL_ENV "__VENV_DIR__"
|
setenv VIRTUAL_ENV __VENV_DIR__
|
||||||
|
|
||||||
set _OLD_VIRTUAL_PATH="$PATH"
|
set _OLD_VIRTUAL_PATH="$PATH"
|
||||||
setenv PATH "$VIRTUAL_ENV/__VENV_BIN_NAME__:$PATH"
|
setenv PATH "$VIRTUAL_ENV/"__VENV_BIN_NAME__":$PATH"
|
||||||
setenv VIRTUAL_ENV_PROMPT "__VENV_PROMPT__"
|
setenv VIRTUAL_ENV_PROMPT __VENV_PROMPT__
|
||||||
|
|
||||||
|
|
||||||
set _OLD_VIRTUAL_PROMPT="$prompt"
|
set _OLD_VIRTUAL_PROMPT="$prompt"
|
||||||
|
|
||||||
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
|
if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then
|
||||||
set prompt = "(__VENV_PROMPT__) $prompt:q"
|
set prompt = "("__VENV_PROMPT__") $prompt:q"
|
||||||
endif
|
endif
|
||||||
|
|
||||||
alias pydoc python -m pydoc
|
alias pydoc python -m pydoc
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
Properly quote template strings in :mod:`venv` activation scripts.
|
Loading…
Add table
Add a link
Reference in a new issue