CVE-2024-4030 (Other, published 2024-05-07T21:15Z)
Description: value: 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.

If 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.

This 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.

Severity: nan (CVSS v3 base score: )
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/python/cpython
Files changed:
Doc/library/os.rst
Lib/test/test_os.py
Lib/test/test_tempfile.py
Misc/NEWS.d/next/Windows/2024-05-01-20-57-09.gh-issue-118486.K44KJG.rst
Modules/posixmodule.c
Commit message: gh-118486: Support mkdir(mode=0o700) on Windows (GH-118488)



Vulnerable code:
    result = CreateDirectoryW(path->wide, NULL);
    if (!result)

Fixed code:
   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.

   .. versionchanged:: 3.13
      Windows now handles a *mode* of ``0o700``.

    @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)

import subprocess
    @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)

:func:`os.mkdir` now accepts *mode* of ``0o700`` to restrict the new
directory to the current user.
#  include <aclapi.h>             // SetEntriesInAcl
#  include <sddl.h>               // SDDL_REVISION_1
#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

#ifdef MS_WINDOWS
    int error = 0;
    int pathError = 0;
    SECURITY_ATTRIBUTES *pSecAttr = NULL;
    struct _Py_SECURITY_ATTRIBUTE_DATA secAttrData;
#endif
    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);
    }
    if (error) {
        PyErr_SetFromWindowsErr(error);
        return NULL;
    }
    if (!result) {
    }