CVE-2024-4030 (published 2024-05-07T21:15Z)

On Windows a directory returned by tempfile.mkdtemp() would not always have permissions set to restrict reading and writing to the temporary directory by other users, instead usually inheriting the correct permissions from the default location. Alternate configurations or users without a profile directory may not have the intended permissions.\n\nIf you’re not using Windows or haven’t changed the temporary directory location then you aren’t affected by this vulnerability. On other platforms the returned directory is consistently readable and writable only by the current user.\n\nThis issue was caused by Python not supporting Unix permissions on Windows. The fix adds support for Unix “700” for the mkdir function on Windows which is used by mkdtemp() to ensure the newly created directory has the proper permissions.\n


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/python/cpython
commit_details: repo_url: https://github.com/python/cpython
author: Steve Dower
author_date: 2024-05-02 15:20:43+01:00
msg: gh-118486: Support mkdir(mode=0o700) on Windows (GH-118488)
repository: repo_url: https://github.com/python/cpython
repo_name: python/cpython
description: The Python programming language
repo_language: Python
owner: python
file_changes: filename: os.rst
change_type: ModificationType.MODIFY
diff: @@ -2430,6 +2430,10 @@ features:
    platform-dependent.  On some platforms, they are ignored and you should call
    :func:`chmod` explicitly to set them.
 
+   On Windows, a *mode* of ``0o700`` is specifically handled to apply access
+   control to the new directory such that only the current user and
+   administrators have access. Other values of *mode* are ignored.
+
    This function can also support :ref:`paths relative to directory descriptors
    <dir_fd>`.
 
@@ -2444,6 +2448,9 @@ features:
    .. versionchanged:: 3.6
       Accepts a :term:`path-like object`.
 
+   .. versionchanged:: 3.13
+      Windows now handles a *mode* of ``0o700``.
+
 
 .. function:: makedirs(name, mode=0o777, exist_ok=False)
 

programming_language: Markdown
filename: test_os.py
change_type: ModificationType.MODIFY
diff: @@ -1811,6 +1811,25 @@ def test_exist_ok_existing_regular_file(self):
         self.assertRaises(OSError, os.makedirs, path, exist_ok=True)
         os.remove(path)
 
+    @unittest.skipUnless(os.name == 'nt', "requires Windows")
+    def test_win32_mkdir_700(self):
+        base = os_helper.TESTFN
+        path1 = os.path.join(os_helper.TESTFN, 'dir1')
+        path2 = os.path.join(os_helper.TESTFN, 'dir2')
+        # mode=0o700 is special-cased to override ACLs on Windows
+        # There's no way to know exactly how the ACLs will look, so we'll
+        # check that they are different from a regularly created directory.
+        os.mkdir(path1, mode=0o700)
+        os.mkdir(path2, mode=0o777)
+
+        out1 = subprocess.check_output(["icacls.exe", path1], encoding="oem")
+        out2 = subprocess.check_output(["icacls.exe", path2], encoding="oem")
+        os.rmdir(path1)
+        os.rmdir(path2)
+        out1 = out1.replace(path1, "<PATH>")
+        out2 = out2.replace(path2, "<PATH>")
+        self.assertNotEqual(out1, out2)
+
     def tearDown(self):
         path = os.path.join(os_helper.TESTFN, 'dir1', 'dir2', 'dir3',
                             'dir4', 'dir5', 'dir6')

programming_language: Python
filename: test_tempfile.py
change_type: ModificationType.MODIFY
diff: @@ -13,6 +13,7 @@
 import weakref
 import gc
 import shutil
+import subprocess
 from unittest import mock
 
 import unittest
@@ -803,6 +804,33 @@ def test_mode(self):
         finally:
             os.rmdir(dir)
 
+    @unittest.skipUnless(os.name == "nt", "Only on Windows.")
+    def test_mode_win32(self):
+        # Use icacls.exe to extract the users with some level of access
+        # Main thing we are testing is that the BUILTIN\Users group has
+        # no access. The exact ACL is going to vary based on which user
+        # is running the test.
+        dir = self.do_create()
+        try:
+            out = subprocess.check_output(["icacls.exe", dir], encoding="oem").casefold()
+        finally:
+            os.rmdir(dir)
+
+        dir = dir.casefold()
+        users = set()
+        found_user = False
+        for line in out.strip().splitlines():
+            acl = None
+            # First line of result includes our directory
+            if line.startswith(dir):
+                acl = line.removeprefix(dir).strip()
+            elif line and line[:1].isspace():
+                acl = line.strip()
+            if acl:
+                users.add(acl.partition(":")[0])
+
+        self.assertNotIn(r"BUILTIN\Users".casefold(), users)
+
     def test_collision_with_existing_file(self):
         # mkdtemp tries another name when a file with
         # the chosen name already exists

programming_language: Python
filename: 2024-05-01-20-57-09.gh-issue-118486.K44KJG.rst
change_type: ModificationType.ADD
diff: @@ -0,0 +1,2 @@
+:func:`os.mkdir` now accepts *mode* of ``0o700`` to restrict the new
+directory to the current user.

programming_language: Markdown
filename: posixmodule.c
change_type: ModificationType.MODIFY
diff: @@ -37,6 +37,8 @@
 #  include <winioctl.h>
 #  include <lmcons.h>             // UNLEN
 #  include "osdefs.h"             // SEP
+#  include <aclapi.h>             // SetEntriesInAcl
+#  include <sddl.h>               // SDDL_REVISION_1
 #  if defined(MS_WINDOWS_DESKTOP) || defined(MS_WINDOWS_SYSTEM)
 #    define HAVE_SYMLINK
 #  endif /* MS_WINDOWS_DESKTOP | MS_WINDOWS_SYSTEM */
@@ -5539,6 +5541,133 @@ os__path_normpath_impl(PyObject *module, PyObject *path)
     return result;
 }
 
+#ifdef MS_WINDOWS
+
+/* We centralise SECURITY_ATTRIBUTE initialization based around
+templates that will probably mostly match common POSIX mode settings.
+The _Py_SECURITY_ATTRIBUTE_DATA structure contains temporary data, as
+a constructed SECURITY_ATTRIBUTE structure typically refers to memory
+that has to be alive while it's being used.
+
+Typical use will look like:
+    SECURITY_ATTRIBUTES *pSecAttr = NULL;
+    struct _Py_SECURITY_ATTRIBUTE_DATA secAttrData;
+    int error, error2;
+
+    Py_BEGIN_ALLOW_THREADS
+    switch (mode) {
+    case 0x1C0: // 0o700
+        error = initializeMkdir700SecurityAttributes(&pSecAttr, &secAttrData);
+        break;
+    ...
+    default:
+        error = initializeDefaultSecurityAttributes(&pSecAttr, &secAttrData);
+        break;
+    }
+
+    if (!error) {
+        // do operation, passing pSecAttr
+    }
+
+    // Unconditionally clear secAttrData.
+    error2 = clearSecurityAttributes(&pSecAttr, &secAttrData);
+    if (!error) {
+        error = error2;
+    }
+    Py_END_ALLOW_THREADS
+
+    if (error) {
+        PyErr_SetFromWindowsErr(error);
+        return NULL;
+    }
+*/
+
+struct _Py_SECURITY_ATTRIBUTE_DATA {
+    SECURITY_ATTRIBUTES securityAttributes;
+    PACL acl;
+    SECURITY_DESCRIPTOR sd;
+    EXPLICIT_ACCESS_W ea[4];
+};
+
+static int
+initializeDefaultSecurityAttributes(
+    PSECURITY_ATTRIBUTES *securityAttributes,
+    struct _Py_SECURITY_ATTRIBUTE_DATA *data
+) {
+    assert(securityAttributes);
+    assert(data);
+    *securityAttributes = NULL;
+    memset(data, 0, sizeof(*data));
+    return 0;
+}
+
+static int
+initializeMkdir700SecurityAttributes(
+    PSECURITY_ATTRIBUTES *securityAttributes,
+    struct _Py_SECURITY_ATTRIBUTE_DATA *data
+) {
+    assert(securityAttributes);
+    assert(data);
+    *securityAttributes = NULL;
+    memset(data, 0, sizeof(*data));
+
+    if (!InitializeSecurityDescriptor(&data->sd, SECURITY_DESCRIPTOR_REVISION)
+        || !SetSecurityDescriptorGroup(&data->sd, NULL, TRUE)) {
+        return GetLastError();
+    }
+
+    data->securityAttributes.nLength = sizeof(SECURITY_ATTRIBUTES);
+    data->ea[0].grfAccessPermissions = GENERIC_ALL;
+    data->ea[0].grfAccessMode = SET_ACCESS;
+    data->ea[0].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT;
+    data->ea[0].Trustee.TrusteeForm = TRUSTEE_IS_NAME;
+    data->ea[0].Trustee.TrusteeType = TRUSTEE_IS_ALIAS;
+    data->ea[0].Trustee.ptstrName = L"CURRENT_USER";
+
+    data->ea[1].grfAccessPermissions = GENERIC_ALL;
+    data->ea[1].grfAccessMode = SET_ACCESS;
+    data->ea[1].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT;
+    data->ea[1].Trustee.TrusteeForm = TRUSTEE_IS_NAME;
+    data->ea[1].Trustee.TrusteeType = TRUSTEE_IS_ALIAS;
+    data->ea[1].Trustee.ptstrName = L"SYSTEM";
+
+    data->ea[2].grfAccessPermissions = GENERIC_ALL;
+    data->ea[2].grfAccessMode = SET_ACCESS;
+    data->ea[2].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT;
+    data->ea[2].Trustee.TrusteeForm = TRUSTEE_IS_NAME;
+    data->ea[2].Trustee.TrusteeType = TRUSTEE_IS_ALIAS;
+    data->ea[2].Trustee.ptstrName = L"ADMINISTRATORS";
+
+    int r = SetEntriesInAclW(3, data->ea, NULL, &data->acl);
+    if (r) {
+        return r;
+    }
+    if (!SetSecurityDescriptorDacl(&data->sd, TRUE, data->acl, FALSE)) {
+        return GetLastError();
+    }
+    data->securityAttributes.lpSecurityDescriptor = &data->sd;
+    *securityAttributes = &data->securityAttributes;
+    return 0;
+}
+
+static int
+clearSecurityAttributes(
+    PSECURITY_ATTRIBUTES *securityAttributes,
+    struct _Py_SECURITY_ATTRIBUTE_DATA *data
+) {
+    assert(securityAttributes);
+    assert(data);
+    *securityAttributes = NULL;
+    if (data->acl) {
+        if (LocalFree((void *)data->acl)) {
+            return GetLastError();
+        }
+    }
+    return 0;
+}
+
+#endif
+
 /*[clinic input]
 os.mkdir
 
@@ -5568,6 +5697,12 @@ os_mkdir_impl(PyObject *module, path_t *path, int mode, int dir_fd)
 /*[clinic end generated code: output=a70446903abe821f input=a61722e1576fab03]*/
 {
     int result;
+#ifdef MS_WINDOWS
+    int error = 0;
+    int pathError = 0;
+    SECURITY_ATTRIBUTES *pSecAttr = NULL;
+    struct _Py_SECURITY_ATTRIBUTE_DATA secAttrData;
+#endif
 #ifdef HAVE_MKDIRAT
     int mkdirat_unavailable = 0;
 #endif
@@ -5579,11 +5714,30 @@ os_mkdir_impl(PyObject *module, path_t *path, int mode, int dir_fd)
 
 #ifdef MS_WINDOWS
     Py_BEGIN_ALLOW_THREADS
-    result = CreateDirectoryW(path->wide, NULL);
+    switch (mode) {
+    case 0x1C0: // 0o700
+        error = initializeMkdir700SecurityAttributes(&pSecAttr, &secAttrData);
+        break;
+    default:
+        error = initializeDefaultSecurityAttributes(&pSecAttr, &secAttrData);
+        break;
+    }
+    if (!error) {
+        result = CreateDirectoryW(path->wide, pSecAttr);
+        error = clearSecurityAttributes(&pSecAttr, &secAttrData);
+    } else {
+        // Ignore error from "clear" - we have a more interesting one already
+        clearSecurityAttributes(&pSecAttr, &secAttrData);
+    }
     Py_END_ALLOW_THREADS
 
-    if (!result)
+    if (error) {
+        PyErr_SetFromWindowsErr(error);
+        return NULL;
+    }
+    if (!result) {
         return path_error(path);
+    }
 #else
     Py_BEGIN_ALLOW_THREADS
 #if HAVE_MKDIRAT

programming_language: C
repo_url: https://github.com/python/cpython
commit_details: repo_url: https://github.com/python/cpython
author: Steve Dower
author_date: 2024-05-02 19:43:54+01:00
msg: gh-118486: Switch mkdir(mode=0o700) on Windows to use OWNER RIGHTS instead of CURRENT_USER (GH-118515)
repository: repo_url: https://github.com/python/cpython
repo_name: python/cpython
description: The Python programming language
repo_language: Python
owner: python
file_changes: filename: posixmodule.c
change_type: ModificationType.MODIFY
diff: @@ -5587,6 +5587,7 @@ struct _Py_SECURITY_ATTRIBUTE_DATA {
     PACL acl;
     SECURITY_DESCRIPTOR sd;
     EXPLICIT_ACCESS_W ea[4];
+    char sid[64];
 };
 
 static int
@@ -5616,13 +5617,25 @@ initializeMkdir700SecurityAttributes(
         return GetLastError();
     }
 
+    int use_alias = 0;
+    DWORD cbSid = sizeof(data->sid);
+    if (!CreateWellKnownSid(WinCreatorOwnerRightsSid, NULL, (PSID)data->sid, &cbSid)) {
+        use_alias = 1;
+    }
+
     data->securityAttributes.nLength = sizeof(SECURITY_ATTRIBUTES);
     data->ea[0].grfAccessPermissions = GENERIC_ALL;
     data->ea[0].grfAccessMode = SET_ACCESS;
     data->ea[0].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT;
-    data->ea[0].Trustee.TrusteeForm = TRUSTEE_IS_NAME;
-    data->ea[0].Trustee.TrusteeType = TRUSTEE_IS_ALIAS;
-    data->ea[0].Trustee.ptstrName = L"CURRENT_USER";
+    if (use_alias) {
+        data->ea[0].Trustee.TrusteeForm = TRUSTEE_IS_NAME;
+        data->ea[0].Trustee.TrusteeType = TRUSTEE_IS_ALIAS;
+        data->ea[0].Trustee.ptstrName = L"CURRENT_USER";
+    } else {
+        data->ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID;
+        data->ea[0].Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP;
+        data->ea[0].Trustee.ptstrName = (LPWCH)(SID*)data->sid;
+    }
 
     data->ea[1].grfAccessPermissions = GENERIC_ALL;
     data->ea[1].grfAccessMode = SET_ACCESS;

programming_language: C
repo_url: https://github.com/python/cpython
commit_details: repo_url: https://github.com/python/cpython
author: Steve Dower
author_date: 2024-05-08 16:52:39+01:00
msg: gh-118486: Update docs for CVE-2024-4030 reference (GH-118737)
repository: repo_url: https://github.com/python/cpython
repo_name: python/cpython
description: The Python programming language
repo_language: Python
owner: python
file_changes: filename: 3.13.rst
change_type: ModificationType.MODIFY
diff: @@ -847,6 +847,12 @@ os
   :c:func:`!posix_spawn_file_actions_addclosefrom_np`.
   (Contributed by Jakub Kulik in :gh:`113117`.)
 
+* :func:`os.mkdir` and :func:`os.makedirs` on Windows now support passing a
+  *mode* value of ``0o700`` to apply access control to the new directory. This
+  implicitly affects :func:`tempfile.mkdtemp` and is a mitigation for
+  :cve:`2024-4030`. Other values for *mode* continue to be ignored.
+  (Contributed by Steve Dower in :gh:`118486`.)
+
 os.path
 -------
 
@@ -989,6 +995,14 @@ sys
   This function is not guaranteed to exist in all implementations of Python.
   (Contributed by Serhiy Storchaka in :gh:`78573`.)
 
+tempfile
+--------
+
+* On Windows, the default mode ``0o700`` used by :func:`tempfile.mkdtemp` now
+  limits access to the new directory due to changes to :func:`os.mkdir`. This
+  is a mitigation for :cve:`2024-4030`.
+  (Contributed by Steve Dower in :gh:`118486`.)
+
 time
 ----
 

programming_language: Python
filename: 2024-05-01-20-57-09.gh-issue-118486.K44KJG.rst
change_type: ModificationType.ADD
diff: @@ -0,0 +1,4 @@
+:func:`os.mkdir` on Windows now accepts *mode* of ``0o700`` to restrict
+the new directory to the current user. This fixes :cve:`2024-4030`
+affecting :func:`tempfile.mkdtemp` in scenarios where the base temporary
+directory is more permissive than the default.

programming_language: Markdown
repo_url: https://github.com/python/cpython
commit_details: repo_url: https://github.com/python/cpython
author: Steve Dower
author_date: 2024-05-15 11:59:41+01:00
msg: gh-118486: Simplify test_win32_mkdir_700 to check the exact ACL (GH-119056)
repository: repo_url: https://github.com/python/cpython
repo_name: python/cpython
description: The Python programming language
repo_language: Python
owner: python
file_changes: filename: test_os.py
change_type: ModificationType.MODIFY
diff: @@ -1814,21 +1814,14 @@ def test_exist_ok_existing_regular_file(self):
     @unittest.skipUnless(os.name == 'nt', "requires Windows")
     def test_win32_mkdir_700(self):
         base = os_helper.TESTFN
-        path1 = os.path.join(os_helper.TESTFN, 'dir1')
-        path2 = os.path.join(os_helper.TESTFN, 'dir2')
-        # mode=0o700 is special-cased to override ACLs on Windows
-        # There's no way to know exactly how the ACLs will look, so we'll
-        # check that they are different from a regularly created directory.
-        os.mkdir(path1, mode=0o700)
-        os.mkdir(path2, mode=0o777)
-
-        out1 = subprocess.check_output(["icacls.exe", path1], encoding="oem")
-        out2 = subprocess.check_output(["icacls.exe", path2], encoding="oem")
-        os.rmdir(path1)
-        os.rmdir(path2)
-        out1 = out1.replace(path1, "<PATH>")
-        out2 = out2.replace(path2, "<PATH>")
-        self.assertNotEqual(out1, out2)
+        path = os.path.abspath(os.path.join(os_helper.TESTFN, 'dir'))
+        os.mkdir(path, mode=0o700)
+        out = subprocess.check_output(["cacls.exe", path, "/s"], encoding="oem")
+        os.rmdir(path)
+        self.assertEqual(
+            out.strip(),
+            f'{path} "D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FA;;;OW)"',
+        )
 
     def tearDown(self):
         path = os.path.join(os_helper.TESTFN, 'dir1', 'dir2', 'dir3',

programming_language: Python
filename: posixmodule.c
change_type: ModificationType.MODIFY
diff: @@ -5590,7 +5590,7 @@ os_mkdir_impl(PyObject *module, path_t *path, int mode, int dir_fd)
     if (mode == 0700 /* 0o700 */) {
         ULONG sdSize;
         pSecAttr = &secAttr;
-        // Set a discreationary ACL (D) that is protected (P) and includes
+        // Set a discretionary ACL (D) that is protected (P) and includes
         // inheritable (OICI) entries that allow (A) full control (FA) to
         // SYSTEM (SY), Administrators (BA), and the owner (OW).
         if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(

programming_language: C