1- using System ;
1+ // Copyright (c) Microsoft Corporation.
2+ // Licensed under the MIT License.
3+
4+ using System ;
25using System . Collections . Generic ;
3- using System . Text ;
6+ using Agent . Sdk . Knob ;
7+ using Microsoft . VisualStudio . Services . Agent . Util ;
8+ using Microsoft . VisualStudio . Services . Agent . Worker ;
9+ using Microsoft . VisualStudio . Services . Common ;
410
511namespace Agent . Worker . Handlers . Helpers
612{
713 public static class ProcessHandlerHelper
814 {
9- public static ( string , CmdTelemetry ) ProcessInputArguments ( string inputArgs )
15+ private const char _escapingSymbol = '^' ;
16+ private const string _envPrefix = "%" ;
17+ private const string _envPostfix = "%" ;
18+
19+ public static ( string , CmdTelemetry ) ExpandCmdEnv ( string inputArgs , Dictionary < string , string > environment )
1020 {
11- const char quote = '"' ;
12- const char escapingSymbol = '^' ;
13- const string envPrefix = "%" ;
14- const string envPostfix = "%" ;
21+ ArgUtil . NotNull ( inputArgs , nameof ( inputArgs ) ) ;
22+ ArgUtil . NotNull ( environment , nameof ( environment ) ) ;
1523
1624 string result = inputArgs ;
1725 int startIndex = 0 ;
1826 var telemetry = new CmdTelemetry ( ) ;
1927
2028 while ( true )
2129 {
22- int prefixIndex = result . IndexOf ( envPrefix , startIndex ) ;
30+ int prefixIndex = result . IndexOf ( _envPrefix , startIndex ) ;
2331 if ( prefixIndex < 0 )
2432 {
2533 break ;
2634 }
2735
2836 telemetry . FoundPrefixes ++ ;
2937
30- if ( prefixIndex > 0 && result [ prefixIndex - 1 ] == escapingSymbol )
38+ if ( prefixIndex > 0 && result [ prefixIndex - 1 ] == _escapingSymbol )
3139 {
32- if ( result [ prefixIndex - 2 ] == 0 || result [ prefixIndex - 2 ] != escapingSymbol )
33- {
34- startIndex ++ ;
35- result = result [ ..( prefixIndex - 1 ) ] + result [ prefixIndex ..] ;
36-
37- telemetry . EscapedVariables ++ ;
38-
39- continue ;
40- }
41-
42- telemetry . EscapedEscapingSymbols ++ ;
40+ telemetry . EscapingSymbolBeforeVar ++ ;
4341 }
4442
45- // We possibly should simplify that part -> if just no close quote, then break
46- int quoteIndex = result . IndexOf ( quote , startIndex ) ;
47- if ( quoteIndex >= 0 && prefixIndex > quoteIndex )
48- {
49- int nextQuoteIndex = result . IndexOf ( quote , quoteIndex + 1 ) ;
50- if ( nextQuoteIndex < 0 )
51- {
52- telemetry . QuotesNotEnclosed = 1 ;
53- break ;
54- }
55-
56- startIndex = nextQuoteIndex + 1 ;
57-
58- telemetry . QuottedBlocks ++ ;
59-
60- continue ;
61- }
62-
63- int envStartIndex = prefixIndex + envPrefix . Length ;
43+ int envStartIndex = prefixIndex + _envPrefix . Length ;
6444 int envEndIndex = FindEnclosingIndex ( result , prefixIndex ) ;
6545 if ( envEndIndex == 0 )
6646 {
@@ -69,32 +49,29 @@ public static (string, CmdTelemetry) ProcessInputArguments(string inputArgs)
6949 }
7050
7151 string envName = result [ envStartIndex ..envEndIndex ] ;
72-
73- telemetry . BracedVariables ++ ;
74-
75- if ( envName . StartsWith ( escapingSymbol ) )
52+ if ( envName . StartsWith ( _escapingSymbol ) )
7653 {
77- var sanitizedEnvName = envPrefix + envName [ 1 ..] + envPostfix ;
78-
79- result = result [ ..prefixIndex ] + sanitizedEnvName + result [ ( envEndIndex + envPostfix . Length ) ..] ;
80- startIndex = prefixIndex + sanitizedEnvName . Length ;
81-
8254 telemetry . VariablesStartsFromES ++ ;
83-
84- continue ;
8555 }
8656
8757 var head = result [ ..prefixIndex ] ;
88- if ( envName . Contains ( escapingSymbol ) )
58+ if ( envName . Contains ( _escapingSymbol , StringComparison . Ordinal ) )
8959 {
90- head += envName . Split ( escapingSymbol ) [ 1 ] ;
91- envName = envName . Split ( escapingSymbol ) [ 0 ] ;
92-
9360 telemetry . VariablesWithESInside ++ ;
9461 }
9562
96- var envValue = System . Environment . GetEnvironmentVariable ( envName ) ?? "" ;
97- var tail = result [ ( envEndIndex + envPostfix . Length ) ..] ;
63+ // Since Windows have case-insensetive environment, and Process handler is windows-specific, we should allign this behavior.
64+ var windowsEnvironment = new Dictionary < string , string > ( environment , StringComparer . OrdinalIgnoreCase ) ;
65+
66+ // In case we don't have such variable, we just leave it as is
67+ if ( ! windowsEnvironment . TryGetValue ( envName , out string envValue ) || string . IsNullOrEmpty ( envValue ) )
68+ {
69+ telemetry . NotExistingEnv ++ ;
70+ startIndex = prefixIndex + 1 ;
71+ continue ;
72+ }
73+
74+ var tail = result [ ( envEndIndex + _envPostfix . Length ) ..] ;
9875
9976 result = head + envValue + tail ;
10077 startIndex = prefixIndex + envValue . Length ;
@@ -119,49 +96,88 @@ private static int FindEnclosingIndex(string input, int targetIndex)
11996
12097 return 0 ;
12198 }
99+
100+ public static ( bool , Dictionary < string , object > ) ValidateInputArguments (
101+ string inputArgs ,
102+ Dictionary < string , string > environment ,
103+ IExecutionContext context )
104+ {
105+ var enableValidation = AgentKnobs . ProcessHandlerSecureArguments . GetValue ( context ) . AsBoolean ( ) ;
106+ context . Debug ( $ "Enable args validation: '{ enableValidation } '") ;
107+ var enableAudit = AgentKnobs . ProcessHandlerSecureArgumentsAudit . GetValue ( context ) . AsBoolean ( ) ;
108+ context . Debug ( $ "Enable args validation audit: '{ enableAudit } '") ;
109+ var enableTelemetry = AgentKnobs . ProcessHandlerTelemetry . GetValue ( context ) . AsBoolean ( ) ;
110+ context . Debug ( $ "Enable telemetry: '{ enableTelemetry } '") ;
111+
112+ if ( enableValidation || enableAudit || enableTelemetry )
113+ {
114+ context . Debug ( "Starting args env expansion" ) ;
115+ var ( expandedArgs , envExpandTelemetry ) = ExpandCmdEnv ( inputArgs , environment ) ;
116+ context . Debug ( $ "Expanded args={ expandedArgs } ") ;
117+
118+ context . Debug ( "Starting args sanitization" ) ;
119+ var ( sanitizedArgs , sanitizeTelemetry ) = CmdArgsSanitizer . SanitizeArguments ( expandedArgs ) ;
120+
121+ Dictionary < string , object > telemetry = null ;
122+ if ( sanitizedArgs != inputArgs )
123+ {
124+ if ( enableTelemetry )
125+ {
126+ telemetry = envExpandTelemetry . ToDictionary ( ) ;
127+ if ( sanitizeTelemetry != null )
128+ {
129+ telemetry . AddRange ( sanitizeTelemetry . ToDictionary ( ) ) ;
130+ }
131+ }
132+ if ( sanitizedArgs != expandedArgs )
133+ {
134+ if ( enableAudit && ! enableValidation )
135+ {
136+ context . Warning ( StringUtil . Loc ( "ProcessHandlerInvalidScriptArgs" ) ) ;
137+ }
138+ if ( enableValidation )
139+ {
140+ return ( false , telemetry ) ;
141+ }
142+
143+ return ( true , telemetry ) ;
144+ }
145+ }
146+
147+ return ( true , null ) ;
148+ }
149+ else
150+ {
151+ context . Debug ( "Args sanitization skipped." ) ;
152+ return ( true , null ) ;
153+ }
154+ }
122155 }
123156
124157 public class CmdTelemetry
125158 {
126159 public int FoundPrefixes { get ; set ; } = 0 ;
127- public int QuottedBlocks { get ; set ; } = 0 ;
128160 public int VariablesExpanded { get ; set ; } = 0 ;
129- public int EscapedVariables { get ; set ; } = 0 ;
130- public int EscapedEscapingSymbols { get ; set ; } = 0 ;
161+ public int EscapingSymbolBeforeVar { get ; set ; } = 0 ;
131162 public int VariablesStartsFromES { get ; set ; } = 0 ;
132- public int BraceSyntaxEntries { get ; set ; } = 0 ;
133- public int BracedVariables { get ; set ; } = 0 ;
134163 public int VariablesWithESInside { get ; set ; } = 0 ;
135164 public int QuotesNotEnclosed { get ; set ; } = 0 ;
136165 public int NotClosedEnvSyntaxPosition { get ; set ; } = 0 ;
166+ public int NotExistingEnv { get ; set ; } = 0 ;
137167
138- public Dictionary < string , int > ToDictionary ( )
168+ public Dictionary < string , object > ToDictionary ( )
139169 {
140- return new Dictionary < string , int >
170+ return new Dictionary < string , object >
141171 {
142172 [ "foundPrefixes" ] = FoundPrefixes ,
143- [ "quottedBlocks" ] = QuottedBlocks ,
144173 [ "variablesExpanded" ] = VariablesExpanded ,
145- [ "escapedVariables" ] = EscapedVariables ,
146- [ "escapedEscapingSymbols" ] = EscapedEscapingSymbols ,
174+ [ "escapedVariables" ] = EscapingSymbolBeforeVar ,
147175 [ "variablesStartsFromES" ] = VariablesStartsFromES ,
148- [ "braceSyntaxEntries" ] = BraceSyntaxEntries ,
149- [ "bracedVariables" ] = BracedVariables ,
150176 [ "bariablesWithESInside" ] = VariablesWithESInside ,
151177 [ "quotesNotEnclosed" ] = QuotesNotEnclosed ,
152- [ "notClosedBraceSyntaxPosition" ] = NotClosedEnvSyntaxPosition
178+ [ "notClosedBraceSyntaxPosition" ] = NotClosedEnvSyntaxPosition ,
179+ [ "notExistingEnv" ] = NotExistingEnv
153180 } ;
154181 }
155-
156- public Dictionary < string , string > ToStringsDictionary ( )
157- {
158- var dict = ToDictionary ( ) ;
159- var result = new Dictionary < string , string > ( ) ;
160- foreach ( var key in dict . Keys )
161- {
162- result . Add ( key , dict [ key ] . ToString ( ) ) ;
163- }
164- return result ;
165- }
166182 } ;
167183}
0 commit comments