@@ -57,7 +57,8 @@ public void TestWorksWithAccessKeyInOptions()
5757 local . setTunnel ( tunnelMock . Object ) ;
5858 Assert . DoesNotThrow ( new TestDelegate ( startWithOptions ) ,
5959 "BROWSERSTACK_ACCESS_KEY cannot be empty. Specify one by adding key to options or adding to the environment variable BROWSERSTACK_ACCESS_KEY." ) ;
60- tunnelMock . Verify ( mock => mock . addBinaryArguments ( It . IsRegex ( "-logFile \" " + logAbsolute + "\" " + "--source \" c-sharp:.*" ) ) , Times . Once ( ) ) ;
60+ tunnelMock . Verify ( mock => mock . addBinaryArguments ( It . Is < List < string > > ( a =>
61+ InOrder ( a , "-logFile" , logAbsolute , "--source" ) && StartsWithAny ( a , "c-sharp:" ) ) ) , Times . Once ( ) ) ;
6162 tunnelMock . Verify ( mock => mock . Run ( "dummyKey" , "" , logAbsolute , "start" ) , Times . Once ( ) ) ;
6263 local . stop ( ) ;
6364 }
@@ -73,7 +74,8 @@ public void TestWorksWithAccessKeyNotInOptions()
7374 local . setTunnel ( tunnelMock . Object ) ;
7475 Assert . DoesNotThrow ( new TestDelegate ( startWithOptions ) ,
7576 "BROWSERSTACK_ACCESS_KEY cannot be empty. Specify one by adding key to options or adding to the environment variable BROWSERSTACK_ACCESS_KEY." ) ;
76- tunnelMock . Verify ( mock => mock . addBinaryArguments ( It . IsRegex ( "-logFile \" " + logAbsolute + "\" .*" ) ) , Times . Once ( ) ) ;
77+ tunnelMock . Verify ( mock => mock . addBinaryArguments ( It . Is < List < string > > ( a =>
78+ InOrder ( a , "-logFile" , logAbsolute ) ) ) , Times . Once ( ) ) ;
7779 tunnelMock . Verify ( mock => mock . Run ( "envDummyKey" , "" , logAbsolute , "start" ) , Times . Once ( ) ) ;
7880 local . stop ( ) ;
7981 }
@@ -90,7 +92,8 @@ public void TestWorksForFolderTesting()
9092 tunnelMock . Setup ( mock => mock . Run ( "dummyKey" , "dummyFolderPath" , logAbsolute , "start" ) ) ;
9193 local . setTunnel ( tunnelMock . Object ) ;
9294 local . start ( options ) ;
93- tunnelMock . Verify ( mock => mock . addBinaryArguments ( It . IsRegex ( "-logFile \" " + logAbsolute + "\" .*" ) ) , Times . Once ( ) ) ;
95+ tunnelMock . Verify ( mock => mock . addBinaryArguments ( It . Is < List < string > > ( a =>
96+ InOrder ( a , "-logFile" , logAbsolute ) ) ) , Times . Once ( ) ) ;
9497 tunnelMock . Verify ( mock => mock . Run ( "dummyKey" , "dummyFolderPath" , logAbsolute , "start" ) , Times . Once ( ) ) ;
9598 local . stop ( ) ;
9699 }
@@ -108,7 +111,8 @@ public void TestWorksForBinaryPath()
108111 local . setTunnel ( tunnelMock . Object ) ;
109112 local . start ( options ) ;
110113 tunnelMock . Verify ( mock => mock . addBinaryPath ( "dummyPath" , "" , It . IsAny < bool > ( ) , It . IsAny < Exception > ( ) ) , Times . Once ) ;
111- tunnelMock . Verify ( mock => mock . addBinaryArguments ( It . IsRegex ( "-logFile \" " + logAbsolute + "\" .*" ) ) , Times . Once ( ) ) ;
114+ tunnelMock . Verify ( mock => mock . addBinaryArguments ( It . Is < List < string > > ( a =>
115+ InOrder ( a , "-logFile" , logAbsolute ) ) ) , Times . Once ( ) ) ;
112116 tunnelMock . Verify ( mock => mock . Run ( "dummyKey" , "" , logAbsolute , "start" ) , Times . Once ( ) ) ;
113117 local . stop ( ) ;
114118 }
@@ -130,7 +134,8 @@ public void TestWorksWithBooleanOptions()
130134 local . setTunnel ( tunnelMock . Object ) ;
131135 local . start ( options ) ;
132136 tunnelMock . Verify ( mock => mock . addBinaryPath ( "" , "" , It . IsAny < bool > ( ) , It . IsAny < Exception > ( ) ) , Times . Once ) ;
133- tunnelMock . Verify ( mock => mock . addBinaryArguments ( It . IsRegex ( "-vvv.*-force.*-forcelocal.*-forceproxy.*-onlyAutomate.*" ) ) , Times . Once ( ) ) ;
137+ tunnelMock . Verify ( mock => mock . addBinaryArguments ( It . Is < List < string > > ( a =>
138+ InOrder ( a , "-vvv" , "-force" , "-forcelocal" , "-forceproxy" , "-onlyAutomate" ) ) ) , Times . Once ( ) ) ;
134139 tunnelMock . Verify ( mock => mock . Run ( "dummyKey" , "" , logAbsolute , "start" ) , Times . Once ( ) ) ;
135140 local . stop ( ) ;
136141 }
@@ -153,8 +158,9 @@ public void TestWorksWithValueOptions()
153158 local . setTunnel ( tunnelMock . Object ) ;
154159 local . start ( options ) ;
155160 tunnelMock . Verify ( mock => mock . addBinaryPath ( "" , "" , It . IsAny < bool > ( ) , It . IsAny < Exception > ( ) ) , Times . Once ) ;
156- tunnelMock . Verify ( mock => mock . addBinaryArguments (
157- It . IsRegex ( "-localIdentifier.*dummyIdentifier.*dummyHost.*-proxyHost.*dummyHost.*-proxyPort.*dummyPort.*-proxyUser.*dummyUser.*-proxyPass.*dummyPass.*" )
161+ tunnelMock . Verify ( mock => mock . addBinaryArguments ( It . Is < List < string > > ( a =>
162+ InOrder ( a , "-localIdentifier" , "dummyIdentifier" , "dummyHost" , "-proxyHost" , "dummyHost" ,
163+ "-proxyPort" , "dummyPort" , "-proxyUser" , "dummyUser" , "-proxyPass" , "dummyPass" ) )
158164 ) , Times . Once ( ) ) ;
159165 tunnelMock . Verify ( mock => mock . Run ( "dummyKey" , "" , logAbsolute , "start" ) , Times . Once ( ) ) ;
160166 local . stop ( ) ;
@@ -176,8 +182,9 @@ public void TestWorksWithCustomOptions()
176182 local . setTunnel ( tunnelMock . Object ) ;
177183 local . start ( options ) ;
178184 tunnelMock . Verify ( mock => mock . addBinaryPath ( "" , "" , It . IsAny < bool > ( ) , It . IsAny < Exception > ( ) ) , Times . Once ) ;
179- tunnelMock . Verify ( mock => mock . addBinaryArguments (
180- It . IsRegex ( "-customBoolKey1.*-customBoolKey2.*-customKey1.*customValue1.*-customKey2.*customValue2.*" )
185+ tunnelMock . Verify ( mock => mock . addBinaryArguments ( It . Is < List < string > > ( a =>
186+ InOrder ( a , "-customBoolKey1" , "-customBoolKey2" , "-customKey1" , "customValue1" ,
187+ "-customKey2" , "customValue2" ) )
181188 ) , Times . Once ( ) ) ;
182189 tunnelMock . Verify ( mock => mock . Run ( "dummyKey" , "" , logAbsolute , "start" ) , Times . Once ( ) ) ;
183190 local . stop ( ) ;
@@ -201,7 +208,8 @@ public void TestCallsFallbackOnFailure()
201208 local . setTunnel ( tunnelMock . Object ) ;
202209 local . start ( options ) ;
203210 tunnelMock . Verify ( mock => mock . addBinaryPath ( "" , "" , It . IsAny < bool > ( ) , It . IsAny < Exception > ( ) ) , Times . Once ) ;
204- tunnelMock . Verify ( mock => mock . addBinaryArguments ( It . IsRegex ( "-logFile \" " + logAbsolute + "\" .*" ) ) , Times . Once ( ) ) ;
211+ tunnelMock . Verify ( mock => mock . addBinaryArguments ( It . Is < List < string > > ( a =>
212+ InOrder ( a , "-logFile" , logAbsolute ) ) ) , Times . Once ( ) ) ;
205213 tunnelMock . Verify ( mock => mock . Run ( "dummyKey" , "" , logAbsolute , "start" ) , Times . Exactly ( 2 ) ) ;
206214 tunnelMock . Verify ( mock => mock . fallbackPaths ( ) , Times . Once ( ) ) ;
207215 local . stop ( ) ;
@@ -220,7 +228,8 @@ public void TestKillsTunnel()
220228 local . start ( options ) ;
221229 local . stop ( ) ;
222230 tunnelMock . Verify ( mock => mock . addBinaryPath ( "" , "" , It . IsAny < bool > ( ) , It . IsAny < Exception > ( ) ) , Times . Once ) ;
223- tunnelMock . Verify ( mock => mock . addBinaryArguments ( It . IsRegex ( "-logFile \" " + logAbsolute + "\" .*" ) ) , Times . Once ( ) ) ;
231+ tunnelMock . Verify ( mock => mock . addBinaryArguments ( It . Is < List < string > > ( a =>
232+ InOrder ( a , "-logFile" , logAbsolute ) ) ) , Times . Once ( ) ) ;
224233 tunnelMock . Verify ( mock => mock . Run ( "dummyKey" , "" , logAbsolute , "start" ) , Times . Once ( ) ) ;
225234 }
226235
@@ -273,6 +282,153 @@ public void TestSetProxyIgnoresInvalidPort()
273282 local . stop ( ) ;
274283 }
275284
285+ // ---- argv helpers -------------------------------------------------------
286+ // Arguments are now discrete argv elements rather than one concatenated string,
287+ // so assertions match elements in order instead of matching a regex.
288+ private static bool InOrder ( List < string > actual , params string [ ] expected )
289+ {
290+ int idx = 0 ;
291+ foreach ( string e in expected )
292+ {
293+ idx = actual . IndexOf ( e , idx ) ;
294+ if ( idx < 0 ) return false ;
295+ idx ++ ;
296+ }
297+ return true ;
298+ }
299+
300+ private static bool StartsWithAny ( List < string > actual , string prefix )
301+ {
302+ return actual . Exists ( a => a != null && a . StartsWith ( prefix ) ) ;
303+ }
304+
305+ // ---- regression tests: CWE-88 argument injection ------------------------
306+ // Each of these fails on the pre-fix code, where every value was concatenated
307+ // into one string that Process.Start then re-tokenised on whitespace.
308+
309+ [ TestMethod ]
310+ public void TestOptionValueWithSpacesStaysOneArgument ( )
311+ {
312+ options = new List < KeyValuePair < string , string > > ( ) ;
313+ options . Add ( new KeyValuePair < string , string > ( "key" , "dummyKey" ) ) ;
314+ options . Add ( new KeyValuePair < string , string > ( "proxyPass" , "p@ss --proxy evil.example.com" ) ) ;
315+
316+ local = new LocalClass ( ) ;
317+ Mock < BrowserStackTunnel > tunnelMock = new Mock < BrowserStackTunnel > ( "test-user-agent" ) ;
318+ local . setTunnel ( tunnelMock . Object ) ;
319+ local . start ( options ) ;
320+
321+ tunnelMock . Verify ( mock => mock . addBinaryArguments ( It . Is < List < string > > ( a =>
322+ InOrder ( a , "-proxyPass" , "p@ss --proxy evil.example.com" )
323+ && ! a . Contains ( "--proxy" ) ) ) , Times . Once ( ) ) ;
324+ local . stop ( ) ;
325+ }
326+
327+ [ TestMethod ]
328+ public void TestUnknownOptionValueWithSpacesStaysOneArgument ( )
329+ {
330+ options = new List < KeyValuePair < string , string > > ( ) ;
331+ options . Add ( new KeyValuePair < string , string > ( "key" , "dummyKey" ) ) ;
332+ options . Add ( new KeyValuePair < string , string > ( "customKey" , "legit --config /tmp/attacker.cfg" ) ) ;
333+
334+ local = new LocalClass ( ) ;
335+ Mock < BrowserStackTunnel > tunnelMock = new Mock < BrowserStackTunnel > ( "test-user-agent" ) ;
336+ local . setTunnel ( tunnelMock . Object ) ;
337+ local . start ( options ) ;
338+
339+ tunnelMock . Verify ( mock => mock . addBinaryArguments ( It . Is < List < string > > ( a =>
340+ InOrder ( a , "-customKey" , "legit --config /tmp/attacker.cfg" )
341+ && ! a . Contains ( "--config" ) ) ) , Times . Once ( ) ) ;
342+ local . stop ( ) ;
343+ }
344+
345+ [ TestMethod ]
346+ public void TestLogFilePathWithQuoteStaysOneArgument ( )
347+ {
348+ options = new List < KeyValuePair < string , string > > ( ) ;
349+ options . Add ( new KeyValuePair < string , string > ( "key" , "dummyKey" ) ) ;
350+ options . Add ( new KeyValuePair < string , string > ( "logfile" , "/tmp/x\" --proxy evil.example.com \" " ) ) ;
351+
352+ local = new LocalClass ( ) ;
353+ Mock < BrowserStackTunnel > tunnelMock = new Mock < BrowserStackTunnel > ( "test-user-agent" ) ;
354+ local . setTunnel ( tunnelMock . Object ) ;
355+ local . start ( options ) ;
356+
357+ tunnelMock . Verify ( mock => mock . addBinaryArguments ( It . Is < List < string > > ( a =>
358+ InOrder ( a , "-logFile" , "/tmp/x\" --proxy evil.example.com \" " )
359+ && ! a . Contains ( "--proxy" ) ) ) , Times . Once ( ) ) ;
360+ local . stop ( ) ;
361+ }
362+
363+ [ TestMethod ]
364+ public void TestAccessKeyWhitespaceIsStrippedFromOptions ( )
365+ {
366+ options = new List < KeyValuePair < string , string > > ( ) ;
367+ options . Add ( new KeyValuePair < string , string > ( "key" , " dummy Key --proxy evil.example.com " ) ) ;
368+
369+ local = new LocalClass ( ) ;
370+ Mock < BrowserStackTunnel > tunnelMock = new Mock < BrowserStackTunnel > ( "test-user-agent" ) ;
371+ local . setTunnel ( tunnelMock . Object ) ;
372+ local . start ( options ) ;
373+
374+ // Whitespace removed, so no "--proxy" token can split out of the key.
375+ tunnelMock . Verify ( mock => mock . Run ( "dummyKey--proxyevil.example.com" , "" , logAbsolute , "start" ) ,
376+ Times . Once ( ) ) ;
377+ local . stop ( ) ;
378+ }
379+
380+ [ TestMethod ]
381+ public void TestAccessKeyWhitespaceIsStrippedFromEnvironmentVariable ( )
382+ {
383+ Environment . SetEnvironmentVariable ( "BROWSERSTACK_ACCESS_KEY" , "env Dummy\t Key" ) ;
384+ options = new List < KeyValuePair < string , string > > ( ) ;
385+
386+ local = new LocalClass ( ) ;
387+ Mock < BrowserStackTunnel > tunnelMock = new Mock < BrowserStackTunnel > ( "test-user-agent" ) ;
388+ local . setTunnel ( tunnelMock . Object ) ;
389+ local . start ( options ) ;
390+
391+ tunnelMock . Verify ( mock => mock . Run ( "envDummyKey" , "" , logAbsolute , "start" ) , Times . Once ( ) ) ;
392+ local . stop ( ) ;
393+ }
394+
395+ [ TestMethod ]
396+ public void TestFolderPathWithSpacesIsPreserved ( )
397+ {
398+ options = new List < KeyValuePair < string , string > > ( ) ;
399+ options . Add ( new KeyValuePair < string , string > ( "key" , "dummyKey" ) ) ;
400+ options . Add ( new KeyValuePair < string , string > ( "f" , "/my/awesome folder" ) ) ;
401+
402+ local = new LocalClass ( ) ;
403+ Mock < BrowserStackTunnel > tunnelMock = new Mock < BrowserStackTunnel > ( "test-user-agent" ) ;
404+ local . setTunnel ( tunnelMock . Object ) ;
405+ local . start ( options ) ;
406+
407+ tunnelMock . Verify ( mock => mock . Run ( "dummyKey" , "/my/awesome folder" , logAbsolute , "start" ) ,
408+ Times . Once ( ) ) ;
409+ local . stop ( ) ;
410+ }
411+
412+ [ TestMethod ]
413+ public void TestDocumentedPassThroughOptionsStillWork ( )
414+ {
415+ options = new List < KeyValuePair < string , string > > ( ) ;
416+ options . Add ( new KeyValuePair < string , string > ( "key" , "dummyKey" ) ) ;
417+ options . Add ( new KeyValuePair < string , string > ( "localProxyHost" , "127.0.0.1" ) ) ;
418+ options . Add ( new KeyValuePair < string , string > ( "localProxyPort" , "8000" ) ) ;
419+ options . Add ( new KeyValuePair < string , string > ( "-pac-file" , "/tmp/my proxy.pac" ) ) ;
420+
421+ local = new LocalClass ( ) ;
422+ Mock < BrowserStackTunnel > tunnelMock = new Mock < BrowserStackTunnel > ( "test-user-agent" ) ;
423+ local . setTunnel ( tunnelMock . Object ) ;
424+ local . start ( options ) ;
425+
426+ tunnelMock . Verify ( mock => mock . addBinaryArguments ( It . Is < List < string > > ( a =>
427+ InOrder ( a , "-localProxyHost" , "127.0.0.1" , "-localProxyPort" , "8000" ,
428+ "--pac-file" , "/tmp/my proxy.pac" ) ) ) , Times . Once ( ) ) ;
429+ local . stop ( ) ;
430+ }
431+
276432 public void startWithOptions ( )
277433 {
278434 local . start ( options ) ;
0 commit comments