@@ -366,10 +366,13 @@ ActionResult Fail(string msg)
366366 // else (advertise subnets, advertise domains) is patched into agent.json *after*
367367 // enrollment, so we don't accumulate parallel CLI surfaces for what is ultimately
368368 // configuration data.
369- string arguments = $ "up --enrollment-string \" { enrollmentString } \" ";
369+ //
370+ // The JWT is passed via stdin (sentinel `-`) to avoid exposing it in the process
371+ // command line (visible to any local process via WMI / Process Explorer / ETW).
372+ string arguments = "up --enrollment-string -" ;
370373 if ( resolvedName . Length != 0 )
371374 {
372- arguments += $ " --name \" { resolvedName } \" ";
375+ arguments += $ " --name { EscapeArg ( resolvedName ) } ";
373376 }
374377
375378 string Redact ( string s ) => s . Replace ( enrollmentString , "***" ) ;
@@ -380,11 +383,17 @@ ActionResult Fail(string msg)
380383 UseShellExecute = false ,
381384 RedirectStandardOutput = true ,
382385 RedirectStandardError = true ,
386+ RedirectStandardInput = true ,
383387 CreateNoWindow = true ,
384388 WorkingDirectory = ProgramDataDirectory ,
385389 } ;
386390
387391 using Process process = Process . Start ( startInfo ) ;
392+
393+ // Write the JWT to stdin and close it so the child sees EOF.
394+ process . StandardInput . Write ( enrollmentString ) ;
395+ process . StandardInput . Close ( ) ;
396+
388397 if ( ! process . WaitForExit ( 60_000 ) )
389398 {
390399 try
@@ -447,6 +456,51 @@ private static bool JwtHasAgentName(string jwt)
447456 }
448457 }
449458
459+ /// <summary>
460+ /// Escape a single argument for the Windows command line using the
461+ /// <c>CommandLineToArgvW</c> rules: internal double-quotes are escaped
462+ /// as <c>\"</c>, backslash runs immediately before a quote are doubled,
463+ /// and the whole value is wrapped in double quotes.
464+ /// </summary>
465+ private static string EscapeArg ( string arg )
466+ {
467+ StringBuilder sb = new ( ) ;
468+ sb . Append ( '"' ) ;
469+
470+ for ( int i = 0 ; i < arg . Length ; i ++ )
471+ {
472+ int backslashes = 0 ;
473+ while ( i < arg . Length && arg [ i ] == '\\ ' )
474+ {
475+ backslashes ++ ;
476+ i ++ ;
477+ }
478+
479+ if ( i == arg . Length )
480+ {
481+ // Trailing backslashes must be doubled because the
482+ // closing quote follows immediately.
483+ sb . Append ( '\\ ' , backslashes * 2 ) ;
484+ break ;
485+ }
486+ else if ( arg [ i ] == '"' )
487+ {
488+ // Backslashes before a double-quote must be doubled,
489+ // plus one extra to escape the quote itself.
490+ sb . Append ( '\\ ' , backslashes * 2 + 1 ) ;
491+ sb . Append ( '"' ) ;
492+ }
493+ else
494+ {
495+ sb . Append ( '\\ ' , backslashes ) ;
496+ sb . Append ( arg [ i ] ) ;
497+ }
498+ }
499+
500+ sb . Append ( '"' ) ;
501+ return sb . ToString ( ) ;
502+ }
503+
450504 /// <summary>
451505 /// Patch the freshly-written agent.json's <c>Tunnel</c> section with the operator's
452506 /// advertised subnets and DNS suffixes from the wizard. Keeping this out of the
0 commit comments