@@ -46,7 +46,9 @@ function LocalBinary(){
4646
4747 let cmd , opts ;
4848 cmd = 'node' ;
49- opts = [ path . join ( __dirname , 'fetchDownloadSourceUrl.js' ) , this . key , this . bsHost ] ;
49+ /* The auth token is handed to the child through its environment, not argv —
50+ argv is readable by any local user via `ps` / /proc/<pid>/cmdline. */
51+ opts = [ path . join ( __dirname , 'fetchDownloadSourceUrl.js' ) , this . bsHost ] ;
5052
5153 if ( retries == 4 || ( this . downloadState . fallbackEnabled && this . parentRetries == 4 ) ) {
5254 opts . push ( true , this . downloadErrorMessage || this . downloadState . errorMessage ) ;
@@ -65,6 +67,9 @@ function LocalBinary(){
6567
6668 const userAgent = [ packageName , version ] . join ( '/' ) ;
6769 const env = Object . assign ( { 'USER_AGENT' : userAgent } , process . env ) ;
70+ if ( this . key ) {
71+ env . BROWSERSTACK_LOCAL_AUTH_TOKEN = this . key ;
72+ }
6873 const obj = childProcess . spawnSync ( cmd , opts , { env : env } ) ;
6974 if ( obj . stdout . length > 0 ) {
7075 this . sourceURL = obj . stdout . toString ( ) . replace ( / \n + $ / , '' ) ;
@@ -147,10 +152,11 @@ function LocalBinary(){
147152 var that = this ;
148153 if ( retries > 0 ) {
149154 console . log ( 'Retrying Download. Retries left' , retries ) ;
150- fs . stat ( binaryPath , function ( err ) {
151- if ( err == null ) {
152- fs . unlinkSync ( binaryPath ) ;
153- }
155+ /* Single unlink instead of stat-then-unlinkSync: the gap between the two
156+ let a concurrent writer swap the file, and a failing unlinkSync threw
157+ out of the stat callback where it could not be caught. A missing file
158+ is the expected case here, so any error is ignored. */
159+ fs . unlink ( binaryPath , function ( ) {
154160 if ( ! callback ) {
155161 return that . downloadSync ( conf , destParentDir , retries - 1 ) ;
156162 }
@@ -322,18 +328,38 @@ function LocalBinary(){
322328 this . getAvailableDirs = function ( ) {
323329 for ( var i = 0 ; i < this . orderedPaths . length ; i ++ ) {
324330 var path = this . orderedPaths [ i ] ;
325- if ( this . makePath ( path ) )
331+ // the last entry lives under the shared temp dir — it must be ours alone
332+ var requirePrivate = ( i === this . orderedPaths . length - 1 ) ;
333+ if ( this . makePath ( path , requirePrivate ) )
326334 return path ;
327335 }
328336 throw new LocalError ( 'Error trying to download BrowserStack Local binary' ) ;
329337 } ;
330338
331- this . makePath = function ( path ) {
339+ this . makePath = function ( path , requirePrivate ) {
332340 try {
333341 if ( ! this . checkPath ( path ) ) {
334- fs . mkdirSync ( path ) ;
342+ fs . mkdirSync ( path , { mode : 0o700 } ) ;
335343 }
336- return true ;
344+ return requirePrivate ? this . isUserPrivateDir ( path ) : true ;
345+ } catch ( e ) {
346+ return false ;
347+ }
348+ } ;
349+
350+ /* Only applied to the shared-temp fallback. The binary is written there and
351+ then executed, so that directory must not be writable by anyone but us —
352+ otherwise another local user can swap the binary between the download and
353+ the exec, or pre-create the path as a symlink. Windows has no POSIX mode
354+ bits; there this is a no-op. */
355+ this . isUserPrivateDir = function ( dirPath ) {
356+ if ( process . platform === 'win32' || typeof process . getuid !== 'function' ) return true ;
357+ try {
358+ var stats = fs . lstatSync ( dirPath ) ;
359+ if ( ! stats . isDirectory ( ) ) return false ;
360+ if ( stats . uid !== process . getuid ( ) ) return false ;
361+ // reject group- or world-writable
362+ return ( stats . mode & 0o022 ) === 0 ;
337363 } catch ( e ) {
338364 return false ;
339365 }
@@ -361,10 +387,18 @@ function LocalBinary(){
361387 return home || null ;
362388 } ;
363389
390+ /* The last entry is a per-user subdirectory of the temp dir rather than the
391+ temp dir itself: os.tmpdir() is /tmp on Linux, which is world-writable, and
392+ the binary name below it is fixed and predictable. */
393+ this . tmpDirPath = function ( ) {
394+ var suffix = ( typeof process . getuid === 'function' ) ? String ( process . getuid ( ) ) : 'user' ;
395+ return path . join ( os . tmpdir ( ) , 'browserstack-local-' + suffix ) ;
396+ } ;
397+
364398 this . orderedPaths = [
365399 path . join ( this . homedir ( ) , '.browserstack' ) ,
366400 process . cwd ( ) ,
367- os . tmpdir ( )
401+ this . tmpDirPath ( )
368402 ] ;
369403}
370404
0 commit comments