Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for denying access to certain files, without inheritance. #14

Merged
merged 1 commit into from
Jun 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading