diff options
| author | tanaka takashi <isazaka@gmail.com> | 2026-08-13 23:20:20 +0900 |
|---|---|---|
| committer | tanaka takashi <isazaka@gmail.com> | 2026-08-13 23:20:20 +0900 |
| commit | e07183ee6d3e57da325213e898cdcd6687320f53 (patch) | |
| tree | a05a71ec3ed419d54f450a0105ec22973c66a66b /src/Agent.Admin/Agent.Admin.Client | |
| parent | 9337ac3e5edd42bb17f5e2ade6c2de02c7ef576d (diff) | |
test(security): close on-demand elevation acceptance gaps
Add an actual-process acceptance harness for the administrative Named Pipe boundary. Exercise production pipe creation and peer PID inspection, remote-form client rejection, Task and protocol identity mismatch, duplicate network application, oversized frames, and an intentional helper-crash recovery point without adding test-only operations to the privileged protocol.
Add a reusable PowerShell attack-matrix runner that preserves the helper parent-image check and drives the self-contained published helper through real UAC. Refactor the client pipe setup into internal diagnostic hooks while leaving the production DACL, PIPE_REJECT_REMOTE_CLIENTS, PID comparison, and helper lifetime behavior unchanged.
Record the completed UAC rejection, HTTP MCP server-continuity, helper crash, startup Recovery rejection/success, loopback restoration, WFP residue, and regression evidence. Promote the three on-demand elevation traceability rows from PARTIAL to PASS and capture the 80-test Windows Security/Recovery result and final Bundle checksum.
Verified: Release build warning/error 0; Core 105 PASS; Planner 10 PASS; administrative boundary 4 PASS; Windows Security/Recovery 80 PASS with skip 0; actual IPC attack matrix PASS; actual Task-time and Recovery-time UAC rejection PASS; Journal/backup/main/helper residue 0; fresh self-contained Bundle show PASS.
Diffstat (limited to 'src/Agent.Admin/Agent.Admin.Client')
| -rw-r--r-- | src/Agent.Admin/Agent.Admin.Client/ElevatedNetworkSession.cs | 35 |
1 files changed, 23 insertions, 12 deletions
diff --git a/src/Agent.Admin/Agent.Admin.Client/ElevatedNetworkSession.cs b/src/Agent.Admin/Agent.Admin.Client/ElevatedNetworkSession.cs index 57339dc..6caa29b 100644 --- a/src/Agent.Admin/Agent.Admin.Client/ElevatedNetworkSession.cs +++ b/src/Agent.Admin/Agent.Admin.Client/ElevatedNetworkSession.cs @@ -41,17 +41,7 @@ public sealed partial class ElevatedNetworkSessionFactory(string helperPath) : I throw new AdminOperationException("sandbox.helper_incompatible", $"Elevated helper does not exist at the fixed Bundle path '{canonicalHelper}'."); string pipeName = $"AgentSandboxMcp-{Environment.ProcessId}-{Guid.NewGuid():N}"; - using WindowsIdentity identity = WindowsIdentity.GetCurrent(); - SecurityIdentifier logon = GetLogonSid(identity); - var security = new PipeSecurity(); - security.SetAccessRuleProtection(isProtected: true, preserveInheritance: false); - security.AddAccessRule(new PipeAccessRule(logon, PipeAccessRights.ReadWrite, AccessControlType.Allow)); - security.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null), - PipeAccessRights.ReadWrite, AccessControlType.Allow)); - security.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.NetworkSid, null), - PipeAccessRights.FullControl, AccessControlType.Deny)); - - NamedPipeServerStream pipe = CreateLocalPipe(pipeName, security); + NamedPipeServerStream pipe = CreateLocalPipeForCurrentLogon(pipeName); Process? helper = null; try { @@ -86,7 +76,7 @@ public sealed partial class ElevatedNetworkSessionFactory(string helperPath) : I await connection.ConfigureAwait(false); linked.Cancel(); - if (!Native.GetNamedPipeClientProcessId(pipe.SafePipeHandle, out uint clientPid) || clientPid != helper.Id || helper.HasExited) + if (!TryGetClientProcessId(pipe, out uint clientPid) || clientPid != helper.Id || helper.HasExited) throw new AdminOperationException("sandbox.ipc_rejected", "The Named Pipe peer does not match the elevated Process started for this Task."); await AdminProtocol.WriteAsync(pipe, new AdminRequest(AdminProtocol.Version, "hello", taskId), cancellationToken).ConfigureAwait(false); AdminResponse hello = await AdminProtocol.ReadAsync<AdminResponse>(pipe, cancellationToken).ConfigureAwait(false); @@ -101,6 +91,24 @@ public sealed partial class ElevatedNetworkSessionFactory(string helperPath) : I } } + internal static NamedPipeServerStream CreateLocalPipeForCurrentLogon(string pipeName) + { + using WindowsIdentity identity = WindowsIdentity.GetCurrent(); + SecurityIdentifier logon = GetLogonSid(identity); + var security = new PipeSecurity(); + security.SetAccessRuleProtection(isProtected: true, preserveInheritance: false); + security.AddAccessRule(new PipeAccessRule(logon, PipeAccessRights.ReadWrite, AccessControlType.Allow)); + security.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.BuiltinAdministratorsSid, null), + PipeAccessRights.ReadWrite, AccessControlType.Allow)); + security.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.NetworkSid, null), + PipeAccessRights.FullControl, AccessControlType.Deny)); + + return CreateLocalPipe(pipeName, security); + } + + internal static bool TryGetClientProcessId(NamedPipeServerStream pipe, out uint clientPid) => + Native.GetNamedPipeClientProcessId(pipe.SafePipeHandle, out clientPid); + private static string Quote(string value) => '"' + value.Replace("\"", "\\\"") + '"'; private static SecurityIdentifier GetLogonSid(WindowsIdentity identity) @@ -186,6 +194,9 @@ internal sealed class ElevatedNetworkSession(NamedPipeServerStream pipe, Process { private bool _cleaned; + internal NamedPipeServerStream Pipe => pipe; + internal Process HelperProcess => helper; + public async Task<bool> InspectLoopbackAsync(string applicationSid, CancellationToken cancellationToken) { AdminResponse response = await SendAsync(new(AdminProtocol.Version, "inspect-loopback", taskId, applicationSid), cancellationToken).ConfigureAwait(false); |
