Skip to content

Commit c8f868d

Browse files
zoobaambv
andauthored
[3.10] gh-118486: Support mkdir(mode=0o700) on Windows (GH-118488) (GH-118740)
Co-authored-by: Łukasz Langa <[email protected]>
1 parent 333c7dc commit c8f868d

File tree

6 files changed

+107
-3
lines changed

6 files changed

+107
-3
lines changed

Doc/library/os.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2065,6 +2065,10 @@ features:
20652065
platform-dependent. On some platforms, they are ignored and you should call
20662066
:func:`chmod` explicitly to set them.
20672067

2068+
On Windows, a *mode* of ``0o700`` is specifically handled to apply access
2069+
control to the new directory such that only the current user and
2070+
administrators have access. Other values of *mode* are ignored.
2071+
20682072
This function can also support :ref:`paths relative to directory descriptors
20692073
<dir_fd>`.
20702074

@@ -2079,6 +2083,9 @@ features:
20792083
.. versionchanged:: 3.6
20802084
Accepts a :term:`path-like object`.
20812085

2086+
.. versionchanged:: 3.10.15
2087+
Windows now handles a *mode* of ``0o700``.
2088+
20822089

20832090
.. function:: makedirs(name, mode=0o777, exist_ok=False)
20842091

Doc/whatsnew/3.10.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1247,6 +1247,13 @@ Add :data:`~os.O_EVTONLY`, :data:`~os.O_FSYNC`, :data:`~os.O_SYMLINK`
12471247
and :data:`~os.O_NOFOLLOW_ANY` for macOS.
12481248
(Contributed by Dong-hee Na in :issue:`43106`.)
12491249
1250+
As of 3.10.15, :func:`os.mkdir` and :func:`os.makedirs` on Windows now support
1251+
passing a *mode* value of ``0o700`` to apply access control to the new
1252+
directory. This implicitly affects :func:`tempfile.mkdtemp` and is a
1253+
mitigation for CVE-2024-4030. Other values for *mode* continue to be
1254+
ignored.
1255+
(Contributed by Steve Dower in :gh:`118486`.)
1256+
12501257
os.path
12511258
-------
12521259
@@ -1399,6 +1406,14 @@ Add :data:`sys.stdlib_module_names`, containing the list of the standard library
13991406
module names.
14001407
(Contributed by Victor Stinner in :issue:`42955`.)
14011408
1409+
tempfile
1410+
--------
1411+
1412+
As of 3.10.15 on Windows, the default mode ``0o700`` used by
1413+
:func:`tempfile.mkdtemp` now limits access to the new directory due to
1414+
changes to :func:`os.mkdir`. This is a mitigation for CVE-2024-4030.
1415+
(Contributed by Steve Dower in :gh:`118486`.)
1416+
14021417
_thread
14031418
-------
14041419

Lib/test/test_os.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1635,6 +1635,18 @@ def test_exist_ok_existing_regular_file(self):
16351635
self.assertRaises(OSError, os.makedirs, path, exist_ok=True)
16361636
os.remove(path)
16371637

1638+
@unittest.skipUnless(os.name == 'nt', "requires Windows")
1639+
def test_win32_mkdir_700(self):
1640+
base = os_helper.TESTFN
1641+
path = os.path.abspath(os.path.join(os_helper.TESTFN, 'dir'))
1642+
os.mkdir(path, mode=0o700)
1643+
out = subprocess.check_output(["cacls.exe", path, "/s"], encoding="oem")
1644+
os.rmdir(path)
1645+
self.assertEqual(
1646+
out.strip(),
1647+
f'{path} "D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FA;;;OW)"',
1648+
)
1649+
16381650
def tearDown(self):
16391651
path = os.path.join(os_helper.TESTFN, 'dir1', 'dir2', 'dir3',
16401652
'dir4', 'dir5', 'dir6')

Lib/test/test_tempfile.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import stat
1212
import types
1313
import weakref
14+
import subprocess
1415
from unittest import mock
1516

1617
import unittest
@@ -800,6 +801,33 @@ def test_mode(self):
800801
finally:
801802
os.rmdir(dir)
802803

804+
@unittest.skipUnless(os.name == "nt", "Only on Windows.")
805+
def test_mode_win32(self):
806+
# Use icacls.exe to extract the users with some level of access
807+
# Main thing we are testing is that the BUILTIN\Users group has
808+
# no access. The exact ACL is going to vary based on which user
809+
# is running the test.
810+
dir = self.do_create()
811+
try:
812+
out = subprocess.check_output(["icacls.exe", dir], encoding="oem").casefold()
813+
finally:
814+
os.rmdir(dir)
815+
816+
dir = dir.casefold()
817+
users = set()
818+
found_user = False
819+
for line in out.strip().splitlines():
820+
acl = None
821+
# First line of result includes our directory
822+
if line.startswith(dir):
823+
acl = line.removeprefix(dir).strip()
824+
elif line and line[:1].isspace():
825+
acl = line.strip()
826+
if acl:
827+
users.add(acl.partition(":")[0])
828+
829+
self.assertNotIn(r"BUILTIN\Users".casefold(), users)
830+
803831
def test_collision_with_existing_file(self):
804832
# mkdtemp tries another name when a file with
805833
# the chosen name already exists
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
:func:`os.mkdir` on Windows now accepts *mode* of ``0o700`` to restrict
2+
the new directory to the current user. This fixes CVE-2024-4030
3+
affecting :func:`tempfile.mkdtemp` in scenarios where the base temporary
4+
directory is more permissive than the default.

Modules/posixmodule.c

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@
3333
#include "pycore_import.h" // _PyImport_ReInitLock()
3434
#include "pycore_initconfig.h" // _PyStatus_EXCEPTION()
3535
#include "pycore_pystate.h" // _PyInterpreterState_GET()
36+
37+
#ifdef MS_WINDOWS
38+
# include <aclapi.h> // SetEntriesInAcl
39+
# include <sddl.h> // SDDL_REVISION_1
40+
#endif
41+
3642
#include "structmember.h" // PyMemberDef
3743
#ifndef MS_WINDOWS
3844
# include "posixmodule.h"
@@ -4465,7 +4471,6 @@ os__path_splitroot_impl(PyObject *module, path_t *path)
44654471

44664472
#endif /* MS_WINDOWS */
44674473

4468-
44694474
/*[clinic input]
44704475
os.mkdir
44714476
@@ -4495,6 +4500,12 @@ os_mkdir_impl(PyObject *module, path_t *path, int mode, int dir_fd)
44954500
/*[clinic end generated code: output=a70446903abe821f input=a61722e1576fab03]*/
44964501
{
44974502
int result;
4503+
#ifdef MS_WINDOWS
4504+
int error = 0;
4505+
int pathError = 0;
4506+
SECURITY_ATTRIBUTES secAttr = { sizeof(secAttr) };
4507+
SECURITY_ATTRIBUTES *pSecAttr = NULL;
4508+
#endif
44984509
#ifdef HAVE_MKDIRAT
44994510
int mkdirat_unavailable = 0;
45004511
#endif
@@ -4506,11 +4517,38 @@ os_mkdir_impl(PyObject *module, path_t *path, int mode, int dir_fd)
45064517

45074518
#ifdef MS_WINDOWS
45084519
Py_BEGIN_ALLOW_THREADS
4509-
result = CreateDirectoryW(path->wide, NULL);
4520+
if (mode == 0700 /* 0o700 */) {
4521+
ULONG sdSize;
4522+
pSecAttr = &secAttr;
4523+
// Set a discretionary ACL (D) that is protected (P) and includes
4524+
// inheritable (OICI) entries that allow (A) full control (FA) to
4525+
// SYSTEM (SY), Administrators (BA), and the owner (OW).
4526+
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(
4527+
L"D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FA;;;OW)",
4528+
SDDL_REVISION_1,
4529+
&secAttr.lpSecurityDescriptor,
4530+
&sdSize
4531+
)) {
4532+
error = GetLastError();
4533+
}
4534+
}
4535+
if (!error) {
4536+
result = CreateDirectoryW(path->wide, pSecAttr);
4537+
if (secAttr.lpSecurityDescriptor &&
4538+
// uncommonly, LocalFree returns non-zero on error, but still uses
4539+
// GetLastError() to see what the error code is
4540+
LocalFree(secAttr.lpSecurityDescriptor)) {
4541+
error = GetLastError();
4542+
}
4543+
}
45104544
Py_END_ALLOW_THREADS
45114545

4512-
if (!result)
4546+
if (error) {
4547+
return PyErr_SetFromWindowsErr(error);
4548+
}
4549+
if (!result) {
45134550
return path_error(path);
4551+
}
45144552
#else
45154553
Py_BEGIN_ALLOW_THREADS
45164554
#if HAVE_MKDIRAT

0 commit comments

Comments
 (0)