Skip to content

Commit

Permalink
Add support for denying access to certain files, without inheritance. (
Browse files Browse the repository at this point in the history
  • Loading branch information
modmuss50 authored Jun 4, 2024
1 parent 3cd9d21 commit 2cbe219
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 12 deletions.
127 changes: 119 additions & 8 deletions windows/Sources/Sandbox/Acl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ public func grantAccess(
_ file: File, appContainer: AppContainer, accessPermissions: [AccessPermissions]
)
throws
{
return try setAccess(file, appContainer: appContainer, accessMode: .grant, accessPermissions: accessPermissions)
}

public func denyAccess(
_ file: File, appContainer: AppContainer, accessPermissions: [AccessPermissions]
)
throws
{
return try setAccess(file, appContainer: appContainer, accessMode: .deny, accessPermissions: accessPermissions)
}

public func setAccess(
_ file: File, appContainer: AppContainer, accessMode: AccessMode, accessPermissions: [AccessPermissions]
)
throws
{
let path = file.path()

Expand All @@ -23,12 +39,11 @@ public func grantAccess(
guard result == ERROR_SUCCESS, let acl = acl else {
throw Win32Error("GetNamedSecurityInfoW")
}
//defer { LocalFree(acl) } // TODO is this needed? seems to crash a lot


var explicitAccess: EXPLICIT_ACCESS_W = EXPLICIT_ACCESS_W(
grfAccessPermissions: accessPermissions.reduce(0) { $0 | $1.rawValue },
grfAccessMode: GRANT_ACCESS,
grfInheritance: DWORD(OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE),
grfAccessMode: accessMode.accessMode,
grfInheritance: accessMode.inheritanceFlags,
Trustee: TRUSTEE_W(
pMultipleTrustee: nil,
MultipleTrusteeOperation: NO_MULTIPLE_TRUSTEE,
Expand All @@ -41,20 +56,31 @@ public func grantAccess(
// Add an entry to the ACL that grants the app container the specified access permissions
var newAcl: PACL? = nil
result = SetEntriesInAclW(1, &explicitAccess, acl, &newAcl)
guard result == ERROR_SUCCESS, let newAcl = newAcl else {
guard result == ERROR_SUCCESS, var newAcl = newAcl else {
throw Win32Error("SetEntriesInAclW")
}
defer { LocalFree(newAcl) }

//print("Granting access to '\(path)' for '\(try appContainer.sidString())'")
if accessMode == .deny {
let _ = try removeFirstAceIf(&newAcl) {
switch $0 {
case .AccessAllowed(let sid):
// Remove any existing access allowed ACEs for the app container
// This likely comes from the parent directory, but we can remove it since inheritance is disabled
return EqualSid(sid, appContainer.sid.value)
default:
return false
}
}
}

// Set the new ACL on the file
result = path.withCString(encodedAs: UTF16.self) { path in
SetNamedSecurityInfoW(
// I dont think this actually mutates the string, at least I hope not
UnsafeMutablePointer(mutating: path),
SE_FILE_OBJECT,
SECURITY_INFORMATION(DACL_SECURITY_INFORMATION),
accessMode.securityInformation,
nil,
nil,
newAcl,
Expand All @@ -65,6 +91,60 @@ public func grantAccess(
throw Win32Error("SetNamedSecurityInfoW '\(path)'", errorCode: result)
}
}

private func removeFirstAceIf(
_ acl: inout PACL, predicate: (Ace) -> Bool
) throws -> Bool {
var aclSize: ACL_SIZE_INFORMATION = ACL_SIZE_INFORMATION()
let success = GetAclInformation(acl, &aclSize, DWORD(MemoryLayout<ACL_SIZE_INFORMATION>.size), AclSizeInformation)
guard success else {
throw Win32Error("GetAclInformation")
}

var toRemove: DWORD? = nil

outer: for i: DWORD in 0..<aclSize.AceCount {
var ace: LPVOID? = nil
let success = GetAce(acl, DWORD(i), &ace)
guard success, let ace = ace else {
throw Win32Error("GetAce")
}

let aceHeader = ace.assumingMemoryBound(to: ACE_HEADER.self).pointee

switch Int32(aceHeader.AceType) {
case ACCESS_ALLOWED_ACE_TYPE:
let accessAllowedAce = ace.assumingMemoryBound(to: ACCESS_ALLOWED_ACE.self).pointee
let sid = SidFromAccessAllowedAce(ace, accessAllowedAce.SidStart)

if predicate(.AccessAllowed(sid!)) {
toRemove = i
break outer
}
case ACCESS_DENIED_ACE_TYPE:
let accessDeniedAce = ace.assumingMemoryBound(to: ACCESS_DENIED_ACE.self).pointee
let sid = SidFromAccessDeniedAce(ace, accessDeniedAce.SidStart)

if predicate(.AccessDenied(sid!)) {
toRemove = i
break outer
}
default:
break
}
}

if let toRemove = toRemove {
let success = DeleteAce(acl, toRemove)
guard success else {
throw Win32Error("DeleteAce")
}
return true
}

return false
}

public func grantNamedPipeAccess(
pipe: NamedPipeServer, appContainer: AppContainer, accessPermissions: [AccessPermissions]
)
Expand All @@ -82,7 +162,6 @@ public func grantNamedPipeAccess(
guard result == ERROR_SUCCESS, let acl = acl else {
throw Win32Error("GetNamedSecurityInfoW")
}
//defer { LocalFree(acl) } // TODO is this needed? seems to crash a lot

var explicitAccess: EXPLICIT_ACCESS_W = EXPLICIT_ACCESS_W(
grfAccessPermissions: accessPermissions.reduce(0) { $0 | $1.rawValue },
Expand Down Expand Up @@ -119,10 +198,42 @@ public func grantNamedPipeAccess(
throw Win32Error("SetSecurityInfo", errorCode: result)
}
}

public enum AccessPermissions: DWORD {
// https://learn.microsoft.com/en-us/windows/win32/secauthz/generic-access-rights
case genericAll = 0x1000_0000
case genericExecute = 0x2000_0000
case genericWrite = 0x4000_0000
case genericRead = 0x8000_0000
}

public enum AccessMode {
case grant
case deny

var accessMode: _ACCESS_MODE {
switch self {
case .grant: return GRANT_ACCESS
case .deny: return DENY_ACCESS
}
}

var inheritanceFlags: DWORD {
switch self {
case .grant: return DWORD(OBJECT_INHERIT_ACE | CONTAINER_INHERIT_ACE)
case .deny: return DWORD(NO_INHERITANCE)
}
}

var securityInformation: SECURITY_INFORMATION {
switch self {
case .grant: return SECURITY_INFORMATION(DACL_SECURITY_INFORMATION)
case .deny: return SECURITY_INFORMATION(UInt32(DACL_SECURITY_INFORMATION) | PROTECTED_DACL_SECURITY_INFORMATION)
}
}
}

public enum Ace {
case AccessAllowed(PSID)
case AccessDenied(PSID)
}
4 changes: 2 additions & 2 deletions windows/Sources/Sandbox/Sid.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,10 @@ public class Sid: CustomStringConvertible {
}

public var description: String {
return (try? getSidString(value)) ?? "Invalid SID"
return (try? Sid.getSidString(value)) ?? "Invalid SID"
}

func getSidString(_ sid: PSID) throws -> String {
static func getSidString(_ sid: PSID) throws -> String {
var sidString: LPWSTR? = nil
let result = ConvertSidToStringSidW(sid, &sidString)

Expand Down
8 changes: 8 additions & 0 deletions windows/Sources/WinSDKExtras/WinSDKExtras.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -86,4 +86,12 @@ DWORD Win32FromHResult(HRESULT hr) {
}
// Not a Win32 HRESULT so return a generic error code.
return ERROR_CAN_NOT_COMPLETE;
}

PSID SidFromAccessAllowedAce(LPVOID ace, DWORD sidStart) {
return &((ACCESS_ALLOWED_ACE*)ace)->SidStart;
}

PSID SidFromAccessDeniedAce(LPVOID ace, DWORD sidStart) {
return &((ACCESS_DENIED_ACE*)ace)->SidStart;
}
6 changes: 5 additions & 1 deletion windows/Sources/WinSDKExtras/include/WinSDKExtras.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,8 @@ LPPROC_THREAD_ATTRIBUTE_LIST allocateAttributeList(size_t dwAttributeCount);

BOOL _IsWindows10OrGreater();

DWORD Win32FromHResult(HRESULT hr);
DWORD Win32FromHResult(HRESULT hr);

PSID SidFromAccessAllowedAce(LPVOID ace, DWORD sidStart);

PSID SidFromAccessDeniedAce(LPVOID ace, DWORD sidStart);
29 changes: 28 additions & 1 deletion windows/Tests/IntergrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,31 @@ import WindowsUtils
#expect(output.contains("Hello, World!"))
}

@Test func testReadFileWithDenyAccess() throws {
let tempDir = try createTempDir()
defer {
try! tempDir.delete()
}

let allowedFile = tempDir.child("allow.txt")
try allowedFile.writeString("Hello, World!")
let deniedFile = tempDir.child("deny.txt")
try deniedFile.writeString("Top secret!")

print(tempDir.path())

let (exitCode, output) = try runIntergration(
["readFile", deniedFile.path()],
filePermissions: [
// Grant read access to the directory
FilePermission(path: tempDir, accessPermissions: [.genericRead]),
// Deny read access to the file
FilePermission(path: deniedFile, accessPermissions: [.genericAll], accessMode: .deny)
])
#expect(exitCode == 0)
#expect(output.contains("Access is denied"))
}

@Test func testRegistryCreate() throws {
let (exitCode, output) = try runIntergration(["registry", "create"])
#expect(exitCode == 0)
Expand Down Expand Up @@ -142,8 +167,9 @@ func runIntergration(
capabilities: capabilities, lpac: lpac)

for filePermission in filePermissions {
try grantAccess(
try setAccess(
filePermission.path, appContainer: container,
accessMode: filePermission.accessMode,
accessPermissions: filePermission.accessPermissions)
}

Expand Down Expand Up @@ -192,6 +218,7 @@ func runIntergration(
struct FilePermission {
var path: File
var accessPermissions: [AccessPermissions]
var accessMode: AccessMode = .grant
}
class TestOutputConsumer: OutputConsumer {
var output = ""
Expand Down

0 comments on commit 2cbe219

Please sign in to comment.