@@ -198,12 +198,29 @@ internal static string WindowsFirewallRemoveCommands() =>
198198 return null ;
199199 }
200200
201- // LocalSystem, LocalService, NetworkService - nobody to own the helper.
201+ // LocalSystem, LocalService, NetworkService - nobody is BEHIND this process, so
202+ // its own SID is the wrong answer twice over: stamping SYSTEM as the owner locks
203+ // out the human who just installed OrgZ, and recording nothing leaves the helper
204+ // fail-open to any authenticated caller.
205+ //
206+ // This is the normal case, not an edge one. The MSI's install hook is deliberately
207+ // NoImpersonate (see scripts/msi-elevate-hooks.ps1) because `sc create` needs the
208+ // elevated token, which means EVERY machine-wide install arrives here. So ask the
209+ // session instead of the token: the person at the console is the person installing.
202210 if ( sid . IsWellKnown ( System . Security . Principal . WellKnownSidType . LocalSystemSid )
203211 || sid . IsWellKnown ( System . Security . Principal . WellKnownSidType . LocalServiceSid )
204212 || sid . IsWellKnown ( System . Security . Principal . WellKnownSidType . NetworkServiceSid ) )
205213 {
206- _log . Information ( "Install is running as a machine account; the device helper will accept any authenticated caller" ) ;
214+ var consoleSid = WindowsConsoleUserSid ( ) ;
215+ if ( consoleSid is not null )
216+ {
217+ _log . Information ( "Install is running as a machine account; recording the console user {Sid} as the device helper's owner" , consoleSid ) ;
218+ return consoleSid ;
219+ }
220+
221+ // Unattended, or nobody logged in - an SCCM push, an image build. There is
222+ // genuinely no owner to record, so this stays the legacy fail-open case.
223+ _log . Warning ( "Install is running as a machine account with no console user; the device helper will accept any authenticated caller" ) ;
207224 return null ;
208225 }
209226
@@ -216,6 +233,76 @@ internal static string WindowsFirewallRemoveCommands() =>
216233 }
217234 }
218235
236+ /// <summary>
237+ /// The SID of the user signed in at the physical console, or null when there isn't one.
238+ ///
239+ /// Used only when the installer itself is a machine account. WTS is asked for the user
240+ /// NAME rather than a token on purpose: <c>WTSQueryUserToken</c> would need SE_TCB_NAME
241+ /// and hand back a handle to own and close, while the name translates to a SID through
242+ /// plain managed code. LocalSystem can read this without any privilege adjustment.
243+ /// </summary>
244+ internal static string ? WindowsConsoleUserSid ( )
245+ {
246+ if ( ! OperatingSystem . IsWindows ( ) )
247+ {
248+ return null ;
249+ }
250+
251+ try
252+ {
253+ var session = WtsNativeMethods . WTSGetActiveConsoleSessionId ( ) ;
254+
255+ // 0xFFFFFFFF means no session is attached to the console - the machine is at the
256+ // lock screen with nobody logged in, or this is a headless/RDP-only box.
257+ if ( session == 0xFFFFFFFF )
258+ {
259+ return null ;
260+ }
261+
262+ var user = WtsQuery ( session , WtsNativeMethods . WtsInfoClass . WTSUserName ) ;
263+ if ( string . IsNullOrWhiteSpace ( user ) )
264+ {
265+ return null ;
266+ }
267+
268+ var domain = WtsQuery ( session , WtsNativeMethods . WtsInfoClass . WTSDomainName ) ;
269+ var account = string . IsNullOrWhiteSpace ( domain ) ? user : $@ "{ domain } \{ user } ";
270+
271+ var identifier = ( System . Security . Principal . SecurityIdentifier )
272+ new System . Security . Principal . NTAccount ( account ) . Translate ( typeof ( System . Security . Principal . SecurityIdentifier ) ) ;
273+
274+ return identifier . Value ;
275+ }
276+ catch ( Exception ex )
277+ {
278+ _log . Debug ( ex , "Could not determine the console user's SID" ) ;
279+ return null ;
280+ }
281+ }
282+
283+ [ System . Runtime . Versioning . SupportedOSPlatform ( "windows" ) ]
284+ private static string ? WtsQuery ( uint session , WtsNativeMethods . WtsInfoClass info )
285+ {
286+ var buffer = IntPtr . Zero ;
287+
288+ try
289+ {
290+ if ( ! WtsNativeMethods . WTSQuerySessionInformationW ( IntPtr . Zero , session , info , out buffer , out _ ) )
291+ {
292+ return null ;
293+ }
294+
295+ return System . Runtime . InteropServices . Marshal . PtrToStringUni ( buffer ) ;
296+ }
297+ finally
298+ {
299+ if ( buffer != IntPtr . Zero )
300+ {
301+ WtsNativeMethods . WTSFreeMemory ( buffer ) ;
302+ }
303+ }
304+ }
305+
219306 /// <summary>
220307 /// Records the owner SID in the service's own environment. A Windows service has no
221308 /// equivalent of launchd's EnvironmentVariables or systemd's Environment=, but the SCM
0 commit comments