Skip to content

Commit 018a779

Browse files
committed
pythongh-118486: Support mkdir(mode=0o700) on Windows
1 parent 40d77b9 commit 018a779

File tree

6 files changed

+246
-2
lines changed

6 files changed

+246
-2
lines changed

Doc/library/os.rst

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

1932+
On Windows, a *mode* of ``0o700`` is specifically handled to apply access
1933+
control to the new directory such that only the current user and
1934+
administrators have access. Other values of *mode* are ignored.
1935+
19321936
This function can also support :ref:`paths relative to directory descriptors
19331937
<dir_fd>`.
19341938

@@ -1943,6 +1947,9 @@ features:
19431947
.. versionchanged:: 3.6
19441948
Accepts a :term:`path-like object`.
19451949

1950+
.. versionchanged:: 3.9.20
1951+
Windows now handles a *mode* of ``0o700``.
1952+
19461953

19471954
.. function:: makedirs(name, mode=0o777, exist_ok=False)
19481955

Doc/whatsnew/3.9.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,13 @@ Added :func:`os.waitstatus_to_exitcode` function:
613613
convert a wait status to an exit code.
614614
(Contributed by Victor Stinner in :issue:`40094`.)
615615

616+
As of 3.9.20, :func:`os.mkdir` and :func:`os.makedirs` on Windows now support
617+
passing a *mode* value of ``0o700`` to apply access control to the new
618+
directory. This implicitly affects :func:`tempfile.mkdtemp` and is a
619+
mitigation for :cve:`2024-4030`. Other values for *mode* continue to be
620+
ignored.
621+
(Contributed by Steve Dower in :gh:`118486`.)
622+
616623
pathlib
617624
-------
618625

@@ -704,6 +711,14 @@ Previously, :attr:`sys.stderr` was block-buffered when non-interactive. Now
704711
``stderr`` defaults to always being line-buffered.
705712
(Contributed by Jendrik Seipp in :issue:`13601`.)
706713

714+
tempfile
715+
--------
716+
717+
As of 3.9.20 on Windows, the default mode ``0o700`` used by
718+
:func:`tempfile.mkdtemp` now limits access to the new directory due to
719+
changes to :func:`os.mkdir`. This is a mitigation for :cve:`2024-4030`.
720+
(Contributed by Steve Dower in :gh:`118486`.)
721+
707722
tracemalloc
708723
-----------
709724

Lib/test/test_os.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1493,6 +1493,25 @@ def test_exist_ok_existing_regular_file(self):
14931493
self.assertRaises(OSError, os.makedirs, path, exist_ok=True)
14941494
os.remove(path)
14951495

1496+
@unittest.skipUnless(os.name == 'nt', "requires Windows")
1497+
def test_win32_mkdir_700(self):
1498+
base = os_helper.TESTFN
1499+
path1 = os.path.join(os_helper.TESTFN, 'dir1')
1500+
path2 = os.path.join(os_helper.TESTFN, 'dir2')
1501+
# mode=0o700 is special-cased to override ACLs on Windows
1502+
# There's no way to know exactly how the ACLs will look, so we'll
1503+
# check that they are different from a regularly created directory.
1504+
os.mkdir(path1, mode=0o700)
1505+
os.mkdir(path2, mode=0o777)
1506+
1507+
out1 = subprocess.check_output(["icacls.exe", path1], encoding="oem")
1508+
out2 = subprocess.check_output(["icacls.exe", path2], encoding="oem")
1509+
os.rmdir(path1)
1510+
os.rmdir(path2)
1511+
out1 = out1.replace(path1, "<PATH>")
1512+
out2 = out2.replace(path2, "<PATH>")
1513+
self.assertNotEqual(out1, out2)
1514+
14961515
def tearDown(self):
14971516
path = os.path.join(support.TESTFN, 'dir1', 'dir2', 'dir3',
14981517
'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
@@ -772,6 +773,33 @@ def test_mode(self):
772773
finally:
773774
os.rmdir(dir)
774775

776+
@unittest.skipUnless(os.name == "nt", "Only on Windows.")
777+
def test_mode_win32(self):
778+
# Use icacls.exe to extract the users with some level of access
779+
# Main thing we are testing is that the BUILTIN\Users group has
780+
# no access. The exact ACL is going to vary based on which user
781+
# is running the test.
782+
dir = self.do_create()
783+
try:
784+
out = subprocess.check_output(["icacls.exe", dir], encoding="oem").casefold()
785+
finally:
786+
os.rmdir(dir)
787+
788+
dir = dir.casefold()
789+
users = set()
790+
found_user = False
791+
for line in out.strip().splitlines():
792+
acl = None
793+
# First line of result includes our directory
794+
if line.startswith(dir):
795+
acl = line.removeprefix(dir).strip()
796+
elif line and line[:1].isspace():
797+
acl = line.strip()
798+
if acl:
799+
users.add(acl.partition(":")[0])
800+
801+
self.assertNotIn(r"BUILTIN\Users".casefold(), users)
802+
775803
def test_collision_with_existing_file(self):
776804
# mkdtemp tries another name when a file with
777805
# 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: 173 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@
2424
#include "pycore_ceval.h" // _PyEval_ReInitThreads()
2525
#include "pycore_import.h" // _PyImport_ReInitLock()
2626
#include "pycore_pystate.h" // _PyInterpreterState_GET()
27+
28+
#ifdef MS_WINDOWS
29+
# include <aclapi.h> // SetEntriesInAcl
30+
# include <sddl.h> // SDDL_REVISION_1
31+
#endif
32+
2733
#include "structmember.h" // PyMemberDef
2834
#ifndef MS_WINDOWS
2935
# include "posixmodule.h"
@@ -4426,6 +4432,146 @@ os__path_splitroot_impl(PyObject *module, path_t *path)
44264432
#endif /* MS_WINDOWS */
44274433

44284434

4435+
#ifdef MS_WINDOWS
4436+
4437+
/* We centralise SECURITY_ATTRIBUTE initialization based around
4438+
templates that will probably mostly match common POSIX mode settings.
4439+
The _Py_SECURITY_ATTRIBUTE_DATA structure contains temporary data, as
4440+
a constructed SECURITY_ATTRIBUTE structure typically refers to memory
4441+
that has to be alive while it's being used.
4442+
4443+
Typical use will look like:
4444+
SECURITY_ATTRIBUTES *pSecAttr = NULL;
4445+
struct _Py_SECURITY_ATTRIBUTE_DATA secAttrData;
4446+
int error, error2;
4447+
4448+
Py_BEGIN_ALLOW_THREADS
4449+
switch (mode) {
4450+
case 0x1C0: // 0o700
4451+
error = initializeMkdir700SecurityAttributes(&pSecAttr, &secAttrData);
4452+
break;
4453+
...
4454+
default:
4455+
error = initializeDefaultSecurityAttributes(&pSecAttr, &secAttrData);
4456+
break;
4457+
}
4458+
4459+
if (!error) {
4460+
// do operation, passing pSecAttr
4461+
}
4462+
4463+
// Unconditionally clear secAttrData.
4464+
error2 = clearSecurityAttributes(&pSecAttr, &secAttrData);
4465+
if (!error) {
4466+
error = error2;
4467+
}
4468+
Py_END_ALLOW_THREADS
4469+
4470+
if (error) {
4471+
PyErr_SetFromWindowsErr(error);
4472+
return NULL;
4473+
}
4474+
*/
4475+
4476+
struct _Py_SECURITY_ATTRIBUTE_DATA {
4477+
SECURITY_ATTRIBUTES securityAttributes;
4478+
PACL acl;
4479+
SECURITY_DESCRIPTOR sd;
4480+
EXPLICIT_ACCESS_W ea[4];
4481+
char sid[64];
4482+
};
4483+
4484+
static int
4485+
initializeDefaultSecurityAttributes(
4486+
PSECURITY_ATTRIBUTES *securityAttributes,
4487+
struct _Py_SECURITY_ATTRIBUTE_DATA *data
4488+
) {
4489+
assert(securityAttributes);
4490+
assert(data);
4491+
*securityAttributes = NULL;
4492+
memset(data, 0, sizeof(*data));
4493+
return 0;
4494+
}
4495+
4496+
static int
4497+
initializeMkdir700SecurityAttributes(
4498+
PSECURITY_ATTRIBUTES *securityAttributes,
4499+
struct _Py_SECURITY_ATTRIBUTE_DATA *data
4500+
) {
4501+
assert(securityAttributes);
4502+
assert(data);
4503+
*securityAttributes = NULL;
4504+
memset(data, 0, sizeof(*data));
4505+
4506+
if (!InitializeSecurityDescriptor(&data->sd, SECURITY_DESCRIPTOR_REVISION)
4507+
|| !SetSecurityDescriptorGroup(&data->sd, NULL, TRUE)) {
4508+
return GetLastError();
4509+
}
4510+
4511+
int use_alias = 0;
4512+
DWORD cbSid = sizeof(data->sid);
4513+
if (!CreateWellKnownSid(WinCreatorOwnerRightsSid, NULL, (PSID)data->sid, &cbSid)) {
4514+
use_alias = 1;
4515+
}
4516+
4517+
data->securityAttributes.nLength = sizeof(SECURITY_ATTRIBUTES);
4518+
data->ea[0].grfAccessPermissions = GENERIC_ALL;
4519+
data->ea[0].grfAccessMode = SET_ACCESS;
4520+
data->ea[0].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT;
4521+
if (use_alias) {
4522+
data->ea[0].Trustee.TrusteeForm = TRUSTEE_IS_NAME;
4523+
data->ea[0].Trustee.TrusteeType = TRUSTEE_IS_ALIAS;
4524+
data->ea[0].Trustee.ptstrName = L"CURRENT_USER";
4525+
} else {
4526+
data->ea[0].Trustee.TrusteeForm = TRUSTEE_IS_SID;
4527+
data->ea[0].Trustee.TrusteeType = TRUSTEE_IS_WELL_KNOWN_GROUP;
4528+
data->ea[0].Trustee.ptstrName = (LPWCH)(SID*)data->sid;
4529+
}
4530+
4531+
data->ea[1].grfAccessPermissions = GENERIC_ALL;
4532+
data->ea[1].grfAccessMode = SET_ACCESS;
4533+
data->ea[1].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT;
4534+
data->ea[1].Trustee.TrusteeForm = TRUSTEE_IS_NAME;
4535+
data->ea[1].Trustee.TrusteeType = TRUSTEE_IS_ALIAS;
4536+
data->ea[1].Trustee.ptstrName = L"SYSTEM";
4537+
4538+
data->ea[2].grfAccessPermissions = GENERIC_ALL;
4539+
data->ea[2].grfAccessMode = SET_ACCESS;
4540+
data->ea[2].grfInheritance = SUB_CONTAINERS_AND_OBJECTS_INHERIT;
4541+
data->ea[2].Trustee.TrusteeForm = TRUSTEE_IS_NAME;
4542+
data->ea[2].Trustee.TrusteeType = TRUSTEE_IS_ALIAS;
4543+
data->ea[2].Trustee.ptstrName = L"ADMINISTRATORS";
4544+
4545+
int r = SetEntriesInAclW(3, data->ea, NULL, &data->acl);
4546+
if (r) {
4547+
return r;
4548+
}
4549+
if (!SetSecurityDescriptorDacl(&data->sd, TRUE, data->acl, FALSE)) {
4550+
return GetLastError();
4551+
}
4552+
data->securityAttributes.lpSecurityDescriptor = &data->sd;
4553+
*securityAttributes = &data->securityAttributes;
4554+
return 0;
4555+
}
4556+
4557+
static int
4558+
clearSecurityAttributes(
4559+
PSECURITY_ATTRIBUTES *securityAttributes,
4560+
struct _Py_SECURITY_ATTRIBUTE_DATA *data
4561+
) {
4562+
assert(securityAttributes);
4563+
assert(data);
4564+
*securityAttributes = NULL;
4565+
if (data->acl) {
4566+
if (LocalFree((void *)data->acl)) {
4567+
return GetLastError();
4568+
}
4569+
}
4570+
return 0;
4571+
}
4572+
4573+
#endif
4574+
44294575
/*[clinic input]
44304576
os.mkdir
44314577
@@ -4454,6 +4600,12 @@ os_mkdir_impl(PyObject *module, path_t *path, int mode, int dir_fd)
44544600
/*[clinic end generated code: output=a70446903abe821f input=e965f68377e9b1ce]*/
44554601
{
44564602
int result;
4603+
#ifdef MS_WINDOWS
4604+
int error = 0;
4605+
int pathError = 0;
4606+
SECURITY_ATTRIBUTES *pSecAttr = NULL;
4607+
struct _Py_SECURITY_ATTRIBUTE_DATA secAttrData;
4608+
#endif
44574609
#ifdef HAVE_MKDIRAT
44584610
int mkdirat_unavailable = 0;
44594611
#endif
@@ -4465,11 +4617,30 @@ os_mkdir_impl(PyObject *module, path_t *path, int mode, int dir_fd)
44654617

44664618
#ifdef MS_WINDOWS
44674619
Py_BEGIN_ALLOW_THREADS
4468-
result = CreateDirectoryW(path->wide, NULL);
4620+
switch (mode) {
4621+
case 0x1C0: // 0o700
4622+
error = initializeMkdir700SecurityAttributes(&pSecAttr, &secAttrData);
4623+
break;
4624+
default:
4625+
error = initializeDefaultSecurityAttributes(&pSecAttr, &secAttrData);
4626+
break;
4627+
}
4628+
if (!error) {
4629+
result = CreateDirectoryW(path->wide, pSecAttr);
4630+
error = clearSecurityAttributes(&pSecAttr, &secAttrData);
4631+
} else {
4632+
// Ignore error from "clear" - we have a more interesting one already
4633+
clearSecurityAttributes(&pSecAttr, &secAttrData);
4634+
}
44694635
Py_END_ALLOW_THREADS
44704636

4471-
if (!result)
4637+
if (error) {
4638+
PyErr_SetFromWindowsErr(error);
4639+
return NULL;
4640+
}
4641+
if (!result) {
44724642
return path_error(path);
4643+
}
44734644
#else
44744645
Py_BEGIN_ALLOW_THREADS
44754646
#if HAVE_MKDIRAT

0 commit comments

Comments
 (0)