Skip to content

Commit 35c799d

Browse files
authored
[3.11] gh-118486: Support mkdir(mode=0o700) on Windows (GH-118488) (GH-118739)
1 parent 4762b36 commit 35c799d

File tree

6 files changed

+103
-2
lines changed

6 files changed

+103
-2
lines changed

Doc/library/os.rst

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

2181+
On Windows, a *mode* of ``0o700`` is specifically handled to apply access
2182+
control to the new directory such that only the current user and
2183+
administrators have access. Other values of *mode* are ignored.
2184+
21812185
This function can also support :ref:`paths relative to directory descriptors
21822186
<dir_fd>`.
21832187

@@ -2192,6 +2196,9 @@ features:
21922196
.. versionchanged:: 3.6
21932197
Accepts a :term:`path-like object`.
21942198

2199+
.. versionchanged:: 3.11.10
2200+
Windows now handles a *mode* of ``0o700``.
2201+
21952202

21962203
.. function:: makedirs(name, mode=0o777, exist_ok=False)
21972204

Doc/whatsnew/3.11.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -899,6 +899,13 @@ os
899899
instead of ``CryptGenRandom()`` which is deprecated.
900900
(Contributed by Donghee Na in :issue:`44611`.)
901901

902+
* As of 3.11.10, :func:`os.mkdir` and :func:`os.makedirs` on Windows
903+
now support passing a *mode* value of ``0o700`` to apply access
904+
control to the new directory. This implicitly affects
905+
:func:`tempfile.mkdtemp` and is a mitigation for CVE-2024-4030.
906+
Other values for *mode* continue to be ignored.
907+
(Contributed by Steve Dower in :gh:`118486`.)
908+
902909

903910
.. _whatsnew311-pathlib:
904911

@@ -1059,6 +1066,11 @@ tempfile
10591066
such as compression modules.
10601067
(Contributed by Carey Metcalfe in :gh:`70363`.)
10611068

1069+
* As of 3.11.10 on Windows, the default mode ``0o700`` used by
1070+
:func:`tempfile.mkdtemp` now limits access to the new directory due to
1071+
changes to :func:`os.mkdir`. This is a mitigation for CVE-2024-4030.
1072+
(Contributed by Steve Dower in :gh:`118486`.)
1073+
10621074

10631075
.. _whatsnew311-threading:
10641076

Lib/test/test_os.py

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

1746+
@unittest.skipUnless(os.name == 'nt', "requires Windows")
1747+
def test_win32_mkdir_700(self):
1748+
base = os_helper.TESTFN
1749+
path = os.path.abspath(os.path.join(os_helper.TESTFN, 'dir'))
1750+
os.mkdir(path, mode=0o700)
1751+
out = subprocess.check_output(["cacls.exe", path, "/s"], encoding="oem")
1752+
os.rmdir(path)
1753+
self.assertEqual(
1754+
out.strip(),
1755+
f'{path} "D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FA;;;OW)"',
1756+
)
1757+
17461758
def tearDown(self):
17471759
path = os.path.join(os_helper.TESTFN, 'dir1', 'dir2', 'dir3',
17481760
'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
@@ -801,6 +802,33 @@ def test_mode(self):
801802
finally:
802803
os.rmdir(dir)
803804

805+
@unittest.skipUnless(os.name == "nt", "Only on Windows.")
806+
def test_mode_win32(self):
807+
# Use icacls.exe to extract the users with some level of access
808+
# Main thing we are testing is that the BUILTIN\Users group has
809+
# no access. The exact ACL is going to vary based on which user
810+
# is running the test.
811+
dir = self.do_create()
812+
try:
813+
out = subprocess.check_output(["icacls.exe", dir], encoding="oem").casefold()
814+
finally:
815+
os.rmdir(dir)
816+
817+
dir = dir.casefold()
818+
users = set()
819+
found_user = False
820+
for line in out.strip().splitlines():
821+
acl = None
822+
# First line of result includes our directory
823+
if line.startswith(dir):
824+
acl = line.removeprefix(dir).strip()
825+
elif line and line[:1].isspace():
826+
acl = line.strip()
827+
if acl:
828+
users.add(acl.partition(":")[0])
829+
830+
self.assertNotIn(r"BUILTIN\Users".casefold(), users)
831+
804832
def test_collision_with_existing_file(self):
805833
# mkdtemp tries another name when a file with
806834
# 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: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@
3434
#include "pycore_pystate.h" // _PyInterpreterState_GET()
3535
#include "pycore_signal.h" // Py_NSIG
3636

37+
#ifdef MS_WINDOWS
38+
# include <aclapi.h> // SetEntriesInAcl
39+
# include <sddl.h> // SDDL_REVISION_1
40+
#endif
41+
3742
#include "structmember.h" // PyMemberDef
3843
#ifndef MS_WINDOWS
3944
# include "posixmodule.h"
@@ -4611,6 +4616,12 @@ os_mkdir_impl(PyObject *module, path_t *path, int mode, int dir_fd)
46114616
/*[clinic end generated code: output=a70446903abe821f input=a61722e1576fab03]*/
46124617
{
46134618
int result;
4619+
#ifdef MS_WINDOWS
4620+
int error = 0;
4621+
int pathError = 0;
4622+
SECURITY_ATTRIBUTES secAttr = { sizeof(secAttr) };
4623+
SECURITY_ATTRIBUTES *pSecAttr = NULL;
4624+
#endif
46144625
#ifdef HAVE_MKDIRAT
46154626
int mkdirat_unavailable = 0;
46164627
#endif
@@ -4622,11 +4633,38 @@ os_mkdir_impl(PyObject *module, path_t *path, int mode, int dir_fd)
46224633

46234634
#ifdef MS_WINDOWS
46244635
Py_BEGIN_ALLOW_THREADS
4625-
result = CreateDirectoryW(path->wide, NULL);
4636+
if (mode == 0700 /* 0o700 */) {
4637+
ULONG sdSize;
4638+
pSecAttr = &secAttr;
4639+
// Set a discretionary ACL (D) that is protected (P) and includes
4640+
// inheritable (OICI) entries that allow (A) full control (FA) to
4641+
// SYSTEM (SY), Administrators (BA), and the owner (OW).
4642+
if (!ConvertStringSecurityDescriptorToSecurityDescriptorW(
4643+
L"D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;BA)(A;OICI;FA;;;OW)",
4644+
SDDL_REVISION_1,
4645+
&secAttr.lpSecurityDescriptor,
4646+
&sdSize
4647+
)) {
4648+
error = GetLastError();
4649+
}
4650+
}
4651+
if (!error) {
4652+
result = CreateDirectoryW(path->wide, pSecAttr);
4653+
if (secAttr.lpSecurityDescriptor &&
4654+
// uncommonly, LocalFree returns non-zero on error, but still uses
4655+
// GetLastError() to see what the error code is
4656+
LocalFree(secAttr.lpSecurityDescriptor)) {
4657+
error = GetLastError();
4658+
}
4659+
}
46264660
Py_END_ALLOW_THREADS
46274661

4628-
if (!result)
4662+
if (error) {
4663+
return PyErr_SetFromWindowsErr(error);
4664+
}
4665+
if (!result) {
46294666
return path_error(path);
4667+
}
46304668
#else
46314669
Py_BEGIN_ALLOW_THREADS
46324670
#if HAVE_MKDIRAT

0 commit comments

Comments
 (0)