Search code examples
pythonwindowsaclpywin32dacl

CreateFileW for READ_CONTROL fails with "Access is denied" despite being owner of the file


On Windows, even if discretionary ACL (DACL) is empty, i.e. no one has permission to the file, file owner can read and write DACL (READ_CONTROL and WRITE_DAC access).

So I tried to do the following:

  1. Set an empty DACL on a file
  2. Obtain a handle to the file for READ_CONTROL
  3. Obtain security descriptor using GetSecurityInfo and the handle
  4. Check that the DACL is actually empty

However, obtaining the handle using CreateFileW failed with Access is denied error. Surprisingly, GetFileSecurity, which is an equivalent of GetSecurityInfo for files, worked fine. According to the documentation, GetFileSecurity requires READ_CONTROL access.

Why does CreateFileW fail in the following example?

import sys
import win32security
import win32con
import win32file
import ntsecuritycon
import os

path = sys.argv[1]

with open(path, "w"):
    pass  # I am the owner of the file

print("Set empty ACL")
sd = win32security.GetFileSecurity(path, win32security.DACL_SECURITY_INFORMATION)
dacl = win32security.ACL()
sd.SetSecurityDescriptorDacl(1, dacl, 0)
win32security.SetFileSecurity(path, win32security.DACL_SECURITY_INFORMATION, sd)

try:
    print("Ensure that ACL is empty with GetFileSecurity")
    sd = win32security.GetFileSecurity(path, win32security.DACL_SECURITY_INFORMATION)
    dacl = sd.GetSecurityDescriptorDacl()
    assert 0 == dacl.GetAceCount()

    print("Try to ensure that ACL is empty using handle")
    handle = win32file.CreateFileW(
        path,
        ntsecuritycon.READ_CONTROL,
        0,
        None,  # security attributes
        win32con.OPEN_EXISTING,
        0,
        None,
    )
    sd = win32security.GetSecurityInfo(handle, win32security.SE_FILE_OBJECT, win32security.DACL_SECURITY_INFORMATION)
    dacl = sd.GetSecurityDescriptorDacl()
    assert 0 == dacl.GetAceCount()
except Exception as e:
    print("FAILURE:", e)
finally:
    print("Restore inherited ACEs before removing file")
    dacl = win32security.ACL()
    win32security.SetNamedSecurityInfo(
        path, 
        win32security.SE_FILE_OBJECT, 
        win32security.DACL_SECURITY_INFORMATION,
        None,
        None,
        dacl,
        None
    )
    os.unlink(path)

Output:

> python acl-test.py file
Set empty ACL
Ensure that ACL is empty with GetFileSecurity
Try to ensure that ACL is empty using handle
FAILURE: (5, 'CreateFileW', 'Access is denied.')
Restore inherited ACEs before removing file

Solution

  • CreateFileW internally calls NtCreateFile with the DesiredAccess parameter passed as dwDesiredAccess | FILE_READ_ATTRIBUTES | SYNCHRONIZE. Thus if you pass dwDesiredAccess as READ_CONTROL, then it actually tries to open the file with READ_CONTROL | FILE_READ_ATTRIBUTES | SYNCHRONIZE access. FILE_READ_ATTRIBUTES access is granted implicitly by the file system if the caller has FILE_LIST_DIRECTORY access for the parent folder. However, SYNCHRONIZE access will not be granted if the file has an empty DACL.

    One solution here would be to use NtOpenFile or NtCreateFile in order to control the exact requested access.