Skip to content

Commit 91e3669

Browse files
zoobaambv
andauthored
[3.8] gh-118486: Support mkdir(mode=0o700) on Windows (GH-118488) (GH-118742)
Co-authored-by: Łukasz Langa <[email protected]>
1 parent 29c9728 commit 91e3669

File tree

6 files changed

+108
-3
lines changed

6 files changed

+108
-3
lines changed

Doc/library/os.rst

+7
Original file line numberDiff line numberDiff line change
@@ -1909,6 +1909,10 @@ features:
19091909
platform-dependent. On some platforms, they are ignored and you should call
19101910
:func:`chmod` explicitly to set them.
19111911

1912+
On Windows, a *mode* of ``0o700`` is specifically handled to apply access
1913+
control to the new directory such that only the current user and
1914+
administrators have access. Other values of *mode* are ignored.
1915+
19121916
This function can also support :ref:`paths relative to directory descriptors
19131917
<dir_fd>`.
19141918

@@ -1923,6 +1927,9 @@ features:
19231927
.. versionchanged:: 3.6
19241928
Accepts a :term:`path-like object`.
19251929

1930+
.. versionchanged:: 3.8.20
1931+
Windows now handles a *mode* of ``0o700``.
1932+
19261933

19271934
.. function:: makedirs(name, mode=0o777, exist_ok=False)
19281935

Doc/whatsnew/3.8.rst

+16
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,13 @@ treat junctions as links.
10461046

10471047
(Contributed by Steve Dower in :issue:`37834`.)
10481048

1049+
As of 3.8.20, :func:`os.mkdir` and :func:`os.makedirs` on Windows now support
1050+
passing a *mode* value of ``0o700`` to apply access control to the new
1051+
directory. This implicitly affects :func:`tempfile.mkdtemp` and is a
1052+
mitigation for CVE-2024-4030. Other values for *mode* continue to be
1053+
ignored.
1054+
(Contributed by Steve Dower in :gh:`118486`.)
1055+
10491056

10501057
os.path
10511058
-------
@@ -1252,6 +1259,15 @@ in a standardized and extensible format, and offers several other benefits.
12521259
(Contributed by C.A.M. Gerlach in :issue:`36268`.)
12531260

12541261

1262+
tempfile
1263+
--------
1264+
1265+
As of 3.8.20 on Windows, the default mode ``0o700`` used by
1266+
:func:`tempfile.mkdtemp` now limits access to the new directory due to
1267+
changes to :func:`os.mkdir`. This is a mitigation for CVE-2024-4030.
1268+
(Contributed by Steve Dower in :gh:`118486`.)
1269+
1270+
12551271
threading
12561272
---------
12571273

Lib/test/test_os.py

+12
Original file line numberDiff line numberDiff line change
@@ -1380,6 +1380,18 @@ def test_exist_ok_existing_regular_file(self):
13801380
self.assertRaises(OSError, os.makedirs, path, exist_ok=True)
13811381
os.remove(path)
13821382

1383+
@unittest.skipUnless(os.name == 'nt', "requires Windows")
1384+
def test_win32_mkdir_700(self):
1385+
base = support.TESTFN
1386+
path = os.path.abspath(os.path.join(support.TESTFN, 'dir'))
1387+
os.mkdir(path, mode=0o700)
1388+
out = subprocess.check_output(["cacls.exe", path, "/s"], encoding="oem")
1389+
os.rmdir(path)
1390+
self.assertEqual(
1391+
out.strip(),
1392+
f'{path} "D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FA;;;OW)"',
1393+
)
1394+
13831395
def tearDown(self):
13841396
path = os.path.join(support.TESTFN, 'dir1', 'dir2', 'dir3',
13851397
'dir4', 'dir5', 'dir6')

Lib/test/test_tempfile.py

+28
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import contextlib
1212
import stat
1313
import weakref
14+
import subprocess
1415
from unittest import mock
1516

1617
import unittest
@@ -760,6 +761,33 @@ def test_mode(self):
760761
finally:
761762
os.rmdir(dir)
762763

764+
@unittest.skipUnless(os.name == "nt", "Only on Windows.")
765+
def test_mode_win32(self):
766+
# Use icacls.exe to extract the users with some level of access
767+
# Main thing we are testing is that the BUILTIN\Users group has
768+
# no access. The exact ACL is going to vary based on which user
769+
# is running the test.
770+
dir = self.do_create()
771+
try:
772+
out = subprocess.check_output(["icacls.exe", dir], encoding="oem").casefold()
773+
finally:
774+
os.rmdir(dir)
775+
776+
dir = dir.casefold()
777+
users = set()
778+
found_user = False
779+
for line in out.strip().splitlines():
780+
acl = None
781+
# First line of result includes our directory
782+
if line.startswith(dir):
783+
acl = line[len(dir):].strip()
784+
elif line and line[:1].isspace():
785+
acl = line.strip()
786+
if acl:
787+
users.add(acl.partition(":")[0])
788+
789+
self.assertNotIn(r"BUILTIN\Users".casefold(), users)
790+
763791
def test_collision_with_existing_file(self):
764792
# mkdtemp tries another name when a file with
765793
# the chosen name already exists
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

+41-3
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@
4040
#include "pycore_pystate.h" /* _PyRuntime */
4141
#include "pythread.h"
4242
#include "structmember.h"
43+
44+
#ifdef MS_WINDOWS
45+
# include <aclapi.h> // SetEntriesInAcl
46+
# include <sddl.h> // SDDL_REVISION_1
47+
#endif
48+
4349
#ifndef MS_WINDOWS
4450
# include "posixmodule.h"
4551
#else
@@ -4122,7 +4128,6 @@ os__path_splitroot_impl(PyObject *module, path_t *path)
41224128

41234129
#endif /* MS_WINDOWS */
41244130

4125-
41264131
/*[clinic input]
41274132
os.mkdir
41284133
@@ -4151,6 +4156,12 @@ os_mkdir_impl(PyObject *module, path_t *path, int mode, int dir_fd)
41514156
/*[clinic end generated code: output=a70446903abe821f input=e965f68377e9b1ce]*/
41524157
{
41534158
int result;
4159+
#ifdef MS_WINDOWS
4160+
int error = 0;
4161+
int pathError = 0;
4162+
SECURITY_ATTRIBUTES secAttr = { sizeof(secAttr) };
4163+
SECURITY_ATTRIBUTES *pSecAttr = NULL;
4164+
#endif
41544165

41554166
if (PySys_Audit("os.mkdir", "Oii", path->object, mode,
41564167
dir_fd == DEFAULT_DIR_FD ? -1 : dir_fd) < 0) {
@@ -4159,11 +4170,38 @@ os_mkdir_impl(PyObject *module, path_t *path, int mode, int dir_fd)
41594170

41604171
#ifdef MS_WINDOWS
41614172
Py_BEGIN_ALLOW_THREADS
4162-
result = CreateDirectoryW(path->wide, NULL);
4173+
if (mode == 0700 /* 0o700 */) {
4174+
ULONG sdSize;
4175+
pSecAttr = &secAttr;
4176+
// Set a discretionary ACL (D) that is protected (P) and includes
4177+
// inheritable (OICI) entries that allow (A) full control (FA) to
4178+
// SYSTEM (SY), Administrators (BA), and the owner (OW).
4179+
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(
4180+
L"D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FA;;;OW)",
4181+
SDDL_REVISION_1,
4182+
&secAttr.lpSecurityDescriptor,
4183+
&sdSize
4184+
)) {
4185+
error = GetLastError();
4186+
}
4187+
}
4188+
if (!error) {
4189+
result = CreateDirectoryW(path->wide, pSecAttr);
4190+
if (secAttr.lpSecurityDescriptor &&
4191+
// uncommonly, LocalFree returns non-zero on error, but still uses
4192+
// GetLastError() to see what the error code is
4193+
LocalFree(secAttr.lpSecurityDescriptor)) {
4194+
error = GetLastError();
4195+
}
4196+
}
41634197
Py_END_ALLOW_THREADS
41644198

4165-
if (!result)
4199+
if (error) {
4200+
return PyErr_SetFromWindowsErr(error);
4201+
}
4202+
if (!result) {
41664203
return path_error(path);
4204+
}
41674205
#else
41684206
Py_BEGIN_ALLOW_THREADS
41694207
#if HAVE_MKDIRAT

0 commit comments

Comments
 (0)