Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

bpo-30730: Prevent environment variables injection in subprocess on Windows. #2325

Merged
merged 3 commits into from
Jun 23, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions Lib/subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -1238,8 +1238,12 @@ def _execute_child(self, args, executable, preexec_fn, close_fds,
# and pass it to fork_exec()

if env is not None:
env_list = [os.fsencode(k) + b'=' + os.fsencode(v)
for k, v in env.items()]
env_list = []
for k, v in env.items():
k = os.fsencode(k)
if b'=' in k:
raise ValueError("illegal environment variable name")
env_list.append(k + b'=' + os.fsencode(v))
else:
env_list = None # Use execv instead of execve.
executable = os.fsencode(executable)
Expand Down
40 changes: 40 additions & 0 deletions Lib/test/test_subprocess.py
Original file line number Diff line number Diff line change
Expand Up @@ -655,6 +655,46 @@ def is_env_var_to_ignore(n):
if not is_env_var_to_ignore(k)]
self.assertEqual(child_env_names, [])

def test_invalid_cmd(self):
# null character in the command name
cmd = sys.executable + '\0'
with self.assertRaises(ValueError):
subprocess.Popen([cmd, "-c", "pass"])

# null character in the command argument
with self.assertRaises(ValueError):
subprocess.Popen([sys.executable, "-c", "pass#\0"])

def test_invalid_env(self):
# null character in the enviroment variable name
newenv = os.environ.copy()
newenv["FRUIT\0VEGETABLE"] = "cabbage"
with self.assertRaises(ValueError):
subprocess.Popen([sys.executable, "-c", "pass"], env=newenv)

# null character in the enviroment variable value
newenv = os.environ.copy()
newenv["FRUIT"] = "orange\0VEGETABLE=cabbage"
with self.assertRaises(ValueError):
subprocess.Popen([sys.executable, "-c", "pass"], env=newenv)

# equal character in the enviroment variable name
newenv = os.environ.copy()
newenv["FRUIT=ORANGE"] = "lemon"
with self.assertRaises(ValueError):
subprocess.Popen([sys.executable, "-c", "pass"], env=newenv)

# equal character in the enviroment variable value
newenv = os.environ.copy()
newenv["FRUIT"] = "orange=lemon"
with subprocess.Popen([sys.executable, "-c",
'import sys, os;'
'sys.stdout.write(os.getenv("FRUIT"))'],
stdout=subprocess.PIPE,
env=newenv) as p:
stdout, stderr = p.communicate()
self.assertEqual(stdout, b"orange=lemon")

def test_communicate_stdin(self):
p = subprocess.Popen([sys.executable, "-c",
'import sys;'
Expand Down
3 changes: 3 additions & 0 deletions Misc/NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -374,6 +374,9 @@ Extension Modules
Library
-------

- [Security] bpo-30730: Prevent environment variables injection in subprocess on
Windows. Prevent passing other environment variables and command arguments.

- bpo-21071: struct.Struct.format type is now :class:`str` instead of
:class:`bytes`.

Expand Down
26 changes: 21 additions & 5 deletions Modules/_winapi.c
Original file line number Diff line number Diff line change
Expand Up @@ -744,6 +744,20 @@ getenvironment(PyObject* environment)
"environment can only contain strings");
goto error;
}
if (PyUnicode_FindChar(key, '\0', 0, PyUnicode_GET_LENGTH(key), 1) != -1 ||
PyUnicode_FindChar(value, '\0', 0, PyUnicode_GET_LENGTH(value), 1) != -1)
{
PyErr_SetString(PyExc_ValueError, "embedded null character");
goto error;
}
/* Search from index 1 because on Windows starting '=' is allowed for
defining hidden environment variables. */
if (PyUnicode_GET_LENGTH(key) == 0 ||
PyUnicode_FindChar(key, '=', 1, PyUnicode_GET_LENGTH(key), 1) != -1)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@serhiy-storchaka I am curious about this. Is there a particular version of Windows this works on? We copied this code in PyPy, but it is failing tests for putenv("=hidden", "foo")

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mattip, the system tracks the working directory on each drive using environment variable names that start with "=", such as "=C:". Python's os.chdir() implements this. Such names are accessible via WinAPI GetEnvironmentStringsW, GetEnvironmentVariableW, and SetEnvironmentVariableW. In most contexts such variables are hidden or filtered out, and using them at the application level isn't officially supported. In particular, CRT environment functions such as putenv and getenv do not support them, and they're filtered out of C [_w]environ, from which Python initializes os.environ.

Here's a manual example:

>>> os.path.abspath('Z:')
'Z:\\'
>>> win32api.SetEnvironmentVariable('=Z:', 'Z:\\test_cwd')
>>> os.path.abspath('Z:')
'Z:\\test_cwd'

Or set the drive working directory in the environment of a child process:

>>> os.path.abspath('Z:')
'Z:\\'
>>> environ = os.environ.copy()
>>> environ['=Z:'] = 'Z:\\test_cwd'
>>> subprocess.call('python -c "import os;print(os.path.abspath(\'Z:\'))"', env=environ)
Z:\test_cwd
0

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick reply. So if I understand correctly while the code (in three places: this one and twice in posix_module) may permit the use of =hidden, it is not guaranteed that the subsequent system call will succeed.

Copy link
Contributor

@eryksun eryksun Jul 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Names that begin with "=" always work with the Windows functions GetEnvironmentVariableW and SetEnvironmentVariableW, and they're inherited fine by child processes. For example:

>>> win32api.SetEnvironmentVariable('=spam', 'eggs')
>>> win32api.GetEnvironmentVariable('=spam')
'eggs'
>>> subprocess.call('python -c "import win32api; print(win32api.GetEnvironmentVariable(\'=spam\'))"')
eggs
0

However, such names are not officially supported. IMO, this is primarily because the C runtime filters them out of its [_w]environ global variable and doesn't support them in environment functions such as putenv(). This in turn is because C and POSIX are tightly linked, and POSIX disallows "=" in environment variable names. Python's os.environ, os.getenv(), and os.putenv() are based on the Windows C runtime (i.e. ucrt), so they have the same limits, and probably should since these are C/POSIX constructs. Maybe one day Python will provide native Windows functionality in the os module that directly accesses the environment block of the process and directly exposes the WinAPI GetEnvironmentVariableW and SetEnvironmentVariableW functions. For now, that's only possible with extensions such as PyWin32 or ctypes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. So I will keep PyPy's putenv consistent with CPython: python allows =hidden as a name, but then the putenv syscall will fail.

{
PyErr_SetString(PyExc_ValueError, "illegal environment variable name");
goto error;
}
if (totalsize > PY_SSIZE_T_MAX - PyUnicode_GET_LENGTH(key) - 1) {
PyErr_SetString(PyExc_OverflowError, "environment too long");
goto error;
Expand Down Expand Up @@ -830,7 +844,8 @@ _winapi_CreateProcess_impl(PyObject *module, Py_UNICODE *application_name,
PROCESS_INFORMATION pi;
STARTUPINFOW si;
PyObject* environment;
wchar_t *wenvironment;
const wchar_t *wenvironment;
Py_ssize_t wenvironment_size;

ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
Expand All @@ -846,12 +861,13 @@ _winapi_CreateProcess_impl(PyObject *module, Py_UNICODE *application_name,

if (env_mapping != Py_None) {
environment = getenvironment(env_mapping);
if (! environment)
if (environment == NULL) {
return NULL;
}
/* contains embedded null characters */
wenvironment = PyUnicode_AsUnicode(environment);
if (wenvironment == NULL)
{
Py_XDECREF(environment);
if (wenvironment == NULL) {
Py_DECREF(environment);
return NULL;
}
}
Expand Down
4 changes: 2 additions & 2 deletions Objects/abstract.c
Original file line number Diff line number Diff line change
Expand Up @@ -2558,8 +2558,8 @@ _PySequence_BytesToCharpArray(PyObject* self)
array[i] = NULL;
goto fail;
}
data = PyBytes_AsString(item);
if (data == NULL) {
/* check for embedded null bytes */
if (PyBytes_AsStringAndSize(item, &data, NULL) < 0) {
/* NULL terminate before freeing. */
array[i] = NULL;
goto fail;
Expand Down