diff --git a/appcontainer/ContainerManager.cs b/appcontainer/ContainerManager.cs index a167dda..94f252b 100644 --- a/appcontainer/ContainerManager.cs +++ b/appcontainer/ContainerManager.cs @@ -177,6 +177,25 @@ public static void DeleteProfile(string name) private static readonly SecurityIdentifier AllAppPackagesSid = new SecurityIdentifier("S-1-15-2-1"); + /// + /// Detect whether a DirectorySecurity object has a NULL or empty DACL. + /// A NULL DACL means "everyone has full access" (including AppContainer), + /// so writing back a DACL containing only our AppContainer ACE would + /// *replace* that implicit full-access with a single-entry DACL and wipe + /// all other principals (Administrators, Users, SYSTEM, …). + /// + private static bool IsNullOrEmptyDacl(CommonObjectSecurity security) + { + var sddl = security.GetSecurityDescriptorSddlForm(AccessControlSections.Access); + // A NULL DACL produces no "D:" section at all, or "D:" with no ACEs. + // An empty string after "D:" (e.g. "D:") also means zero ACEs. + int dIdx = sddl.IndexOf("D:", StringComparison.Ordinal); + if (dIdx < 0) return true; // no DACL section → NULL DACL + // Check whether there is at least one ACE "(…)" after the "D:…" flags + int aceStart = sddl.IndexOf('(', dIdx); + return aceStart < 0; // no ACE entries → empty DACL + } + /// /// Check whether a path already has sufficient access for ALL APPLICATION PACKAGES. /// If so, no per-profile ACL grant is needed (just add to settings). @@ -250,6 +269,15 @@ public static void GrantAccess(string containerName, string dirPath, bool readOn var info = new DirectoryInfo(dirPath); var security = info.GetAccessControl(); + // Safety: a NULL/empty DACL means everyone already has full access + // (including AppContainer). Writing back would replace it with a + // single-entry DACL and strip Administrators/Users/SYSTEM. + if (IsNullOrEmptyDacl(security)) + { + Console.Error.WriteLine($"[grant] [{sw.ElapsedMilliseconds}ms] SKIPPED — NULL/empty DACL on dir (all access already implied): {dirPath}"); + return; + } + // Remove existing rules for this SID first (avoid duplicates) security.PurgeAccessRules(sid); @@ -267,6 +295,13 @@ public static void GrantAccess(string containerName, string dirPath, bool readOn { var info = new FileInfo(dirPath); var security = info.GetAccessControl(); + + if (IsNullOrEmptyDacl(security)) + { + Console.Error.WriteLine($"[grant] [{sw.ElapsedMilliseconds}ms] SKIPPED — NULL/empty DACL on file (all access already implied): {dirPath}"); + return; + } + security.PurgeAccessRules(sid); security.AddAccessRule(new FileSystemAccessRule( sid, rights, @@ -580,17 +615,26 @@ public static void GrantAncestorTraverse(string containerName, string dirPath) if (!hasAccess) { - // Traverse + ReadAttributes only (no ListDirectory to avoid exposing - // directory contents — e.g. D:\ sibling folder names are private). - security.AddAccessRule(new FileSystemAccessRule( - sid, - FileSystemRights.ReadAttributes | - FileSystemRights.ReadExtendedAttributes | FileSystemRights.Traverse, - InheritanceFlags.None, - PropagationFlags.None, - AccessControlType.Allow)); - info.SetAccessControl(security); - Console.Error.WriteLine($"[traverse] Granted: {current}"); + // Safety: skip if DACL is NULL/empty — all access is already + // implied and writing back would wipe other principals. + if (IsNullOrEmptyDacl(security)) + { + Console.Error.WriteLine($"[traverse] SKIPPED — NULL/empty DACL (all access already implied): {current}"); + } + else + { + // Traverse + ReadAttributes only (no ListDirectory to avoid exposing + // directory contents — e.g. D:\ sibling folder names are private). + security.AddAccessRule(new FileSystemAccessRule( + sid, + FileSystemRights.ReadAttributes | + FileSystemRights.ReadExtendedAttributes | FileSystemRights.Traverse, + InheritanceFlags.None, + PropagationFlags.None, + AccessControlType.Allow)); + info.SetAccessControl(security); + Console.Error.WriteLine($"[traverse] Granted: {current}"); + } } else { @@ -1192,6 +1236,14 @@ public static void SetupDriveTraverse(string containerName) if (!hasAccess) { + // Safety: skip if DACL is NULL/empty — all access is already + // implied and writing back would wipe other principals. + if (IsNullOrEmptyDacl(security)) + { + Console.Error.WriteLine($"[setup] SKIPPED — NULL/empty DACL (all access already implied): {dir}"); + continue; + } + security.AddAccessRule(new FileSystemAccessRule( sid, FileSystemRights.ReadAttributes |