python-pyinstaller/backport-CVE-2023-49797.patch
2024-06-18 16:48:05 +08:00

514 lines
19 KiB
Diff

From 5a53dc58295b26eb8af1f146df990a657bc916d1 Mon Sep 17 00:00:00 2001
From: Rok Mandeljc <rok.mandeljc@gmail.com>
Date: Sat, 5 Aug 2023 12:50:28 +0200
Subject: [PATCH] rthooks: secure temp directories used by matplotlib and
win32com rthooks
The run-time hooks that relocate the package's configuration/cache
directory into isolated temporary directory create this directory using
the `tempfile.mkdtemp` function. According to its documentation, the
function creates the temporary directory "in the most secure manner
possible", and the created directory should be "readable, writable, and
searchable only by the creating user ID".
However, this does not apply to Windows, where the 0o700 POSIX
permissions mask passed to the underyling `os.mkdir` call has no effect.
Consequently, the access to the created temporary directory is in fact
gated only by the access to the parent directory. So as long as `TEMP`
and `TMP` point to `%LOCALAPPDATA%\Temp`, the created temporary
directories are typically inaccessible to other users, who do not have
access to the user's home directory. On the other hand, if the temporary
directory base is relocated to a system-wide location (e.g., `c:\temp`),
the temporary directories created by the run-time hooks might become
accessible to other users as well. A malicious user with local access
might thus modify the contents of the temporary directory, interfering
with the application. If the application is running in privileged mode
and developer mode is enabled on the system, they might also attempt
a symlink attack due to lack of hardened mode for `shutil.rmtree`
(used for clean up) on Windows.
Therefore, we replace the use of `tempfile.mkdtemp` with custom function
that uses original `mkdtemp` on POSIX and provides a Windows-specific
implementation that secures the access to created directory via security
descriptor passed to the `CreateDirectoryW` call. This is a
`ctypes`-based port of the code that we already have in bootloader for
mitigating the same issue with temporary directory in onefile builds.
In order to share the implementation among the two run-time hooks that
require it, the code is provided by a new `_pyi_rth_utils` PyInstaller
"fake" package, which is bundled with the frozen application on demand
(i.e., if it is referenced in any of collected run-time hooks).
Origin:
https://github.com/pyinstaller/pyinstaller/commit/5a53dc58295b26eb8af1f146df990a657bc916d1
---
MANIFEST.in | 2 +-
.../fake-modules/_pyi_rth_utils/__init__.py | 56 ++++
.../fake-modules/_pyi_rth_utils/_win32.py | 262 ++++++++++++++++++
.../hook-_pyi_rth_utils.py | 25 ++
.../hooks/rthooks/pyi_rth_mplconfig.py | 8 +-
.../hooks/rthooks/pyi_rth_win32comgenpy.py | 8 +-
news/7827.bugfix.rst | 5 +
setup.cfg | 1 +
setup.py | 1 +
9 files changed, 361 insertions(+), 7 deletions(-)
create mode 100644 PyInstaller/fake-modules/_pyi_rth_utils/__init__.py
create mode 100644 PyInstaller/fake-modules/_pyi_rth_utils/_win32.py
create mode 100644 PyInstaller/hooks/pre_find_module_path/hook-_pyi_rth_utils.py
create mode 100644 news/7827.bugfix.rst
diff --git a/MANIFEST.in b/MANIFEST.in
index a77f401..2c702a1 100755
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -12,6 +12,6 @@ recursive-include PyInstaller/bootloader/Windows-64bit-intel *
recursive-include PyInstaller/bootloader/Darwin-64bit *
include pyproject.toml
# These files need to be explicitly included
-include PyInstaller/fake-modules/*.py
+recursive-include PyInstaller/fake-modules *.py
include PyInstaller/hooks/rthooks.dat
include PyInstaller/lib/README.rst
diff --git a/PyInstaller/fake-modules/_pyi_rth_utils/__init__.py b/PyInstaller/fake-modules/_pyi_rth_utils/__init__.py
new file mode 100644
index 0000000..2d3af0e
--- /dev/null
+++ b/PyInstaller/fake-modules/_pyi_rth_utils/__init__.py
@@ -0,0 +1,56 @@
+# -----------------------------------------------------------------------------
+# Copyright (c) 2023, PyInstaller Development Team.
+#
+# Distributed under the terms of the GNU General Public License (version 2
+# or later) with exception for distributing the bootloader.
+#
+# The full license is in the file COPYING.txt, distributed with this software.
+#
+# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
+# -----------------------------------------------------------------------------
+
+import os
+import sys
+import errno
+import tempfile
+
+# Helper for creating temporary directories with access restricted to the user running the process.
+# On POSIX systems, this is already achieved by `tempfile.mkdtemp`, which uses 0o700 permissions mask.
+# On Windows, however, the POSIX permissions semantics have no effect, and we need to provide our own implementation
+# that restricts the access by passing appropriate security attributes to the `CreateDirectory` function.
+
+if os.name == 'nt':
+ from . import _win32
+
+ def secure_mkdtemp(suffix=None, prefix=None, dir=None):
+ """
+ Windows-specific replacement for `tempfile.mkdtemp` that restricts access to the user running the process.
+ Based on `mkdtemp` implementation from python 3.11 stdlib.
+ """
+
+ prefix, suffix, dir, output_type = tempfile._sanitize_params(prefix, suffix, dir)
+
+ names = tempfile._get_candidate_names()
+ if output_type is bytes:
+ names = map(os.fsencode, names)
+
+ for seq in range(tempfile.TMP_MAX):
+ name = next(names)
+ file = os.path.join(dir, prefix + name + suffix)
+ sys.audit("tempfile.mkdtemp", file)
+ try:
+ _win32.secure_mkdir(file)
+ except FileExistsError:
+ continue # try again
+ except PermissionError:
+ # This exception is thrown when a directory with the chosen name already exists on windows.
+ if (os.name == 'nt' and os.path.isdir(dir) and os.access(dir, os.W_OK)):
+ continue
+ else:
+ raise
+ return file
+
+ raise FileExistsError(errno.EEXIST, "No usable temporary directory name found")
+
+else:
+ secure_mkdtemp = tempfile.mkdtemp
diff --git a/PyInstaller/fake-modules/_pyi_rth_utils/_win32.py b/PyInstaller/fake-modules/_pyi_rth_utils/_win32.py
new file mode 100644
index 0000000..41237fb
--- /dev/null
+++ b/PyInstaller/fake-modules/_pyi_rth_utils/_win32.py
@@ -0,0 +1,262 @@
+# -----------------------------------------------------------------------------
+# Copyright (c) 2023, PyInstaller Development Team.
+#
+# Distributed under the terms of the GNU General Public License (version 2
+# or later) with exception for distributing the bootloader.
+#
+# The full license is in the file COPYING.txt, distributed with this software.
+#
+# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
+# -----------------------------------------------------------------------------
+
+import ctypes
+import ctypes.wintypes
+
+# Constants from win32 headers
+TOKEN_QUERY = 0x0008
+
+TokenUser = 1 # from TOKEN_INFORMATION_CLASS enum
+
+ERROR_INSUFFICIENT_BUFFER = 122
+
+INVALID_HANDLE = -1
+
+FORMAT_MESSAGE_ALLOCATE_BUFFER = 0x00000100
+FORMAT_MESSAGE_FROM_SYSTEM = 0x00001000
+
+SDDL_REVISION1 = 1
+
+# Structures for ConvertSidToStringSidW
+PSID = ctypes.wintypes.LPVOID
+
+
+class SID_AND_ATTRIBUTES(ctypes.Structure):
+ _fields_ = [
+ ("Sid", PSID),
+ ("Attributes", ctypes.wintypes.DWORD),
+ ]
+
+
+class TOKEN_USER(ctypes.Structure):
+ _fields_ = [
+ ("User", SID_AND_ATTRIBUTES),
+ ]
+
+
+PTOKEN_USER = ctypes.POINTER(TOKEN_USER)
+
+# SECURITY_ATTRIBUTES structure for CreateDirectoryW
+PSECURITY_DESCRIPTOR = ctypes.wintypes.LPVOID
+
+
+class SECURITY_ATTRIBUTES(ctypes.Structure):
+ _fields_ = [
+ ("nLength", ctypes.wintypes.DWORD),
+ ("lpSecurityDescriptor", PSECURITY_DESCRIPTOR),
+ ("bInheritHandle", ctypes.wintypes.BOOL),
+ ]
+
+
+# win32 API functions, bound via ctypes.
+# NOTE: we do not use ctypes.windll.<dll_name> to avoid modifying its (global) function prototypes, which might affect
+# user's code.
+kernel32 = ctypes.WinDLL("kernel32")
+advapi32 = ctypes.WinDLL("advapi32")
+
+kernel32.CloseHandle.restype = ctypes.wintypes.BOOL
+kernel32.CloseHandle.argtypes = (ctypes.wintypes.HANDLE,)
+
+kernel32.LocalFree.restype = ctypes.wintypes.BOOL
+kernel32.LocalFree.argtypes = (ctypes.wintypes.HLOCAL,)
+
+kernel32.GetCurrentProcess.restype = ctypes.wintypes.HANDLE
+
+kernel32.OpenProcessToken.restype = ctypes.wintypes.BOOL
+kernel32.OpenProcessToken.argtypes = (
+ ctypes.wintypes.HANDLE,
+ ctypes.wintypes.DWORD,
+ ctypes.wintypes.PHANDLE,
+)
+
+advapi32.ConvertSidToStringSidW.restype = ctypes.wintypes.BOOL
+advapi32.ConvertSidToStringSidW.argtypes = (
+ PSID,
+ ctypes.POINTER(ctypes.wintypes.LPWSTR),
+)
+
+advapi32.ConvertStringSecurityDescriptorToSecurityDescriptorW.restype = ctypes.wintypes.BOOL
+advapi32.ConvertStringSecurityDescriptorToSecurityDescriptorW.argtypes = (
+ ctypes.wintypes.LPCWSTR,
+ ctypes.wintypes.DWORD,
+ ctypes.POINTER(PSECURITY_DESCRIPTOR),
+ ctypes.wintypes.PULONG,
+)
+
+
+def _win_error_to_message(error_code):
+ """
+ Convert win32 error code to message.
+ """
+ message_wstr = ctypes.wintypes.LPWSTR(None)
+ ret = kernel32.FormatMessageW(
+ FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
+ None, # lpSource
+ error_code, # dwMessageId
+ 0x400, # dwLanguageId = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT)
+ ctypes.byref(message_wstr), # pointer to LPWSTR due to FORMAT_MESSAGE_ALLOCATE_BUFFER
+ 64, # due to FORMAT_MESSAGE_ALLOCATE_BUFFER, this is minimum number of characters to allocate
+ )
+ if ret == 0:
+ return None
+
+ message = message_wstr.value
+ kernel32.LocalFree(message_wstr)
+
+ # Strip trailing CR/LF.
+ if message:
+ message = message.strip()
+ return message
+
+
+def _get_user_sid():
+ """
+ Obtain the SID for the current user.
+ """
+ process_token = ctypes.wintypes.HANDLE(INVALID_HANDLE)
+
+ try:
+ # Get access token for the current process
+ ret = kernel32.OpenProcessToken(
+ kernel32.GetCurrentProcess(),
+ TOKEN_QUERY,
+ ctypes.pointer(process_token),
+ )
+ if ret == 0:
+ error_code = kernel32.GetLastError()
+ raise RuntimeError(f"Failed to open process token! Error code: 0x{error_code:X}")
+
+ # Query buffer size for user info structure
+ user_info_size = ctypes.wintypes.DWORD(0)
+
+ ret = advapi32.GetTokenInformation(
+ process_token,
+ TokenUser,
+ None,
+ 0,
+ ctypes.byref(user_info_size),
+ )
+
+ # We expect this call to fail with ERROR_INSUFFICIENT_BUFFER
+ if ret == 0:
+ error_code = kernel32.GetLastError()
+ if error_code != ERROR_INSUFFICIENT_BUFFER:
+ raise RuntimeError(f"Failed to query token information buffer size! Error code: 0x{error_code:X}")
+ else:
+ raise RuntimeError("Unexpected return value from GetTokenInformation!")
+
+ # Allocate buffer
+ user_info = ctypes.create_string_buffer(user_info_size.value)
+ ret = advapi32.GetTokenInformation(
+ process_token,
+ TokenUser,
+ user_info,
+ user_info_size,
+ ctypes.byref(user_info_size),
+ )
+ if ret == 0:
+ error_code = kernel32.GetLastError()
+ raise RuntimeError(f"Failed to query token information! Error code: 0x{error_code:X}")
+
+ # Convert SID to string
+ # Technically, we need to pass user_info->User.Sid, but as they are at the beginning of the
+ # buffer, just pass the buffer instead...
+ sid_wstr = ctypes.wintypes.LPWSTR(None)
+ ret = advapi32.ConvertSidToStringSidW(
+ ctypes.cast(user_info, PTOKEN_USER).contents.User.Sid,
+ ctypes.pointer(sid_wstr),
+ )
+ if ret == 0:
+ error_code = kernel32.GetLastError()
+ raise RuntimeError(f"Failed to convert SID to string! Error code: 0x{error_code:X}")
+ sid = sid_wstr.value
+ kernel32.LocalFree(sid_wstr)
+ except Exception:
+ sid = None
+ finally:
+ # Close the process token
+ if process_token.value != INVALID_HANDLE:
+ kernel32.CloseHandle(process_token)
+
+ return sid
+
+
+# Get and cache current user's SID
+_user_sid = _get_user_sid()
+
+
+def secure_mkdir(dir_name):
+ """
+ Replacement for mkdir that limits the access to created directory to current user.
+ """
+
+ # Create security descriptor
+ # Prefer actual user SID over SID S-1-3-4 (current owner), because at the time of writing, Wine does not properly
+ # support the latter.
+ sid = _user_sid or "S-1-3-4"
+
+ # DACL descriptor (D):
+ # ace_type;ace_flags;rights;object_guid;inherit_object_guid;account_sid;(resource_attribute)
+ # - ace_type = SDDL_ACCESS_ALLOWED (A)
+ # - rights = SDDL_FILE_ALL (FA)
+ # - account_sid = current user (queried SID)
+ security_desc_str = f"D:(A;;FA;;;{sid})"
+ security_desc = ctypes.wintypes.LPVOID(None)
+
+ ret = advapi32.ConvertStringSecurityDescriptorToSecurityDescriptorW(
+ security_desc_str,
+ SDDL_REVISION1,
+ ctypes.byref(security_desc),
+ None,
+ )
+ if ret == 0:
+ error_code = kernel32.GetLastError()
+ raise RuntimeError(
+ f"Failed to create security descriptor! Error code: 0x{error_code:X}, "
+ f"message: {_win_error_to_message(error_code)}"
+ )
+
+ security_attr = SECURITY_ATTRIBUTES()
+ security_attr.nLength = ctypes.sizeof(SECURITY_ATTRIBUTES)
+ security_attr.lpSecurityDescriptor = security_desc
+ security_attr.bInheritHandle = False
+
+ # Create directory
+ ret = kernel32.CreateDirectoryW(
+ dir_name,
+ security_attr,
+ )
+ if ret == 0:
+ # Call failed; store error code immediately, to avoid it being overwritten in cleanup below.
+ error_code = kernel32.GetLastError()
+
+ # Free security descriptor
+ kernel32.LocalFree(security_desc)
+
+ # Exit on succeess
+ if ret != 0:
+ return
+
+ # Construct OSError from win error code
+ error_message = _win_error_to_message(error_code)
+
+ # Strip trailing dot to match error message from os.mkdir().
+ if error_message and error_message[-1] == '.':
+ error_message = error_message[:-1]
+
+ raise OSError(
+ None, # errno
+ error_message, # strerror
+ dir_name, # filename
+ error_code, # winerror
+ None, # filename2
+ )
diff --git a/PyInstaller/hooks/pre_find_module_path/hook-_pyi_rth_utils.py b/PyInstaller/hooks/pre_find_module_path/hook-_pyi_rth_utils.py
new file mode 100644
index 0000000..d035df0
--- /dev/null
+++ b/PyInstaller/hooks/pre_find_module_path/hook-_pyi_rth_utils.py
@@ -0,0 +1,25 @@
+# -----------------------------------------------------------------------------
+# Copyright (c) 2023, PyInstaller Development Team.
+#
+# Distributed under the terms of the GNU General Public License (version 2
+# or later) with exception for distributing the bootloader.
+#
+# The full license is in the file COPYING.txt, distributed with this software.
+#
+# SPDX-License-Identifier: (GPL-2.0-or-later WITH Bootloader-exception)
+# -----------------------------------------------------------------------------
+"""
+This hook allows discovery and collection of PyInstaller's internal _pyi_rth_utils module that provides utility
+functions for run-time hooks.
+
+The module is implemented in 'PyInstaller/fake-modules/_pyi_rth_utils.py'.
+"""
+
+import os
+
+from PyInstaller import PACKAGEPATH
+
+
+def pre_find_module_path(api):
+ module_dir = os.path.join(PACKAGEPATH, 'fake-modules')
+ api.search_dirs = [module_dir]
diff --git a/PyInstaller/hooks/rthooks/pyi_rth_mplconfig.py b/PyInstaller/hooks/rthooks/pyi_rth_mplconfig.py
index 018d6fe..6ea9425 100755
--- a/PyInstaller/hooks/rthooks/pyi_rth_mplconfig.py
+++ b/PyInstaller/hooks/rthooks/pyi_rth_mplconfig.py
@@ -27,10 +27,12 @@ def _pyi_rthook():
import atexit
import os
import shutil
- import tempfile
- # Put matplot config dir to temp directory.
- configdir = tempfile.mkdtemp()
+ import _pyi_rth_utils # PyInstaller's run-time hook utilities module
+
+ # Isolate matplotlib's config dir into temporary directory.
+ # Use our replacement for `tempfile.mkdtemp` function that properly restricts access to directory on all platforms.
+ configdir = _pyi_rth_utils.secure_mkdtemp()
os.environ['MPLCONFIGDIR'] = configdir
try:
diff --git a/PyInstaller/hooks/rthooks/pyi_rth_win32comgenpy.py b/PyInstaller/hooks/rthooks/pyi_rth_win32comgenpy.py
index aed2515..a9bbbd1 100755
--- a/PyInstaller/hooks/rthooks/pyi_rth_win32comgenpy.py
+++ b/PyInstaller/hooks/rthooks/pyi_rth_win32comgenpy.py
@@ -21,13 +21,15 @@ def _pyi_rthook():
import atexit
import os
import shutil
- import tempfile
import win32com
- # Create temporary directory. The actual cache directory needs to be named `gen_py`, so create a sub-directory.
- supportdir = tempfile.mkdtemp()
+ import _pyi_rth_utils # PyInstaller's run-time hook utilities module
+ # Create temporary directory.
+ # Use our replacement for `tempfile.mkdtemp` function that properly restricts access to directory on all platforms.
+ supportdir = _pyi_rth_utils.secure_mkdtemp()
+ # The actual cache directory needs to be named `gen_py`, so create a sub-directory.
genpydir = os.path.join(supportdir, 'gen_py')
os.makedirs(genpydir, exist_ok=True)
diff --git a/news/7827.bugfix.rst b/news/7827.bugfix.rst
new file mode 100644
index 0000000..41ea37a
--- /dev/null
+++ b/news/7827.bugfix.rst
@@ -0,0 +1,5 @@
+(Windows) Ensure that the access to temporary directories created by
+the ``matplotlib`` and ``win32com`` run-time hooks is restricted to
+the user running the frozen application, even if ``TMP`` / ``TEMP``
+environment directory points to a system-wide location that can be
+accessed to all users.
diff --git a/setup.cfg b/setup.cfg
index 7cd1548..756ae34 100755
--- a/setup.cfg
+++ b/setup.cfg
@@ -70,6 +70,7 @@ include =
PyInstaller =
bootloader/*/*
fake-modules/*.py
+ fake-modules/_pyi_rth_utils/*.py
hooks/rthooks.dat
lib/README.rst
diff --git a/setup.py b/setup.py
index 51d2482..ac82bb6 100755
--- a/setup.py
+++ b/setup.py
@@ -114,6 +114,7 @@ def finalize_options(self):
*(f"bootloader/images/*.{suffix}" for suffix in self.ICON_TYPES),
# These files need to be explicitly included as well.
"fake-modules/*.py",
+ "fake-modules/_pyi_rth_utils/*.py",
"hooks/rthooks.dat",
"lib/README.rst",
],
--
2.43.0