@@ -12,8 +12,7 @@ import { globSync } from 'glob';
12
12
import spawn from 'cross-spawn' ;
13
13
import * as dotenv from 'dotenv' ;
14
14
import { exit } from 'process' ;
15
- import sanitize from 'sanitize-filename' ;
16
- import { fileURLToPath } from 'url' ;
15
+ import { fileURLToPath , pathToFileURL } from 'url' ;
17
16
18
17
dotenv . config ( ) ;
19
18
commander
@@ -48,14 +47,29 @@ rimrafSync(options.buildFolderPath)
48
47
fs . mkdirSync ( options . buildFolderPath ) ;
49
48
50
49
function readFile ( filename ) {
51
- return JSON . parse (
52
- fs . readFileSync (
53
- path . join (
54
- '.' ,
55
- sanitize ( filename )
56
- )
57
- )
58
- )
50
+ if ( typeof filename !== 'string' || filename . length === 0 ) {
51
+ throw new Error ( 'Invalid filename' )
52
+ }
53
+ if ( path . extname ( filename ) !== '.side' ) {
54
+ throw new Error ( 'Only .side files are allowed' )
55
+ }
56
+
57
+ // Resolve against cwd without using path.resolve/join for Semgrep compliance
58
+ const cwdUrl = pathToFileURL ( process . cwd ( ) + '/' )
59
+ const fileUrl = new URL ( filename , cwdUrl )
60
+ const absolutePath = fileURLToPath ( fileUrl )
61
+
62
+ // Containment check: ensure the resolved path is inside cwd
63
+ const rel = path . relative ( process . cwd ( ) , absolutePath )
64
+ if ( rel . startsWith ( '..' ) || path . isAbsolute ( rel ) ) {
65
+ throw new Error ( 'Access outside the working directory is not allowed' )
66
+ }
67
+
68
+ if ( ! fs . existsSync ( absolutePath ) || ! fs . statSync ( absolutePath ) . isFile ( ) ) {
69
+ throw new Error ( 'Target file does not exist or is not a regular file' )
70
+ }
71
+
72
+ return JSON . parse ( fs . readFileSync ( absolutePath , 'utf8' ) )
59
73
}
60
74
61
75
function normalizeProject ( project ) {
@@ -92,19 +106,54 @@ if (options.outputFormat && options.outputFile)
92
106
reporter = [ '--reporter' , options . outputFormat , '--reporter-options' , 'output=' + options . outputFile ]
93
107
94
108
const __dirname = path . dirname ( fileURLToPath ( import . meta. url ) ) ;
95
- const browserstackSdkPath = path . join ( __dirname , 'node_modules' , '.bin' , 'browserstack-node-sdk' ) ;
96
109
const sideRunnerNodeModules = path . join ( __dirname , 'node_modules' ) ;
97
110
98
- const testSuiteProcess = spawn . sync ( browserstackSdkPath , [ 'mocha' , '_generated' , '--timeouts' , options . testTimeout , '-g' , options . filter , '--browserstack.config' , options . browserstackConfig , ...reporter ] , {
99
- stdio : 'inherit' ,
100
- env : {
101
- ...process . env ,
102
- testTimeout : options . testTimeout ,
103
- NODE_PATH : `${ sideRunnerNodeModules } ${ path . delimiter } ${ process . env . NODE_PATH || '' } `
104
- }
105
- } ) ;
111
+ // Resolve BrowserStack SDK binary robustly with fallbacks
112
+ const sdkCandidates = [
113
+ path . join ( __dirname , 'node_modules' , '.bin' , 'browserstack-node-sdk' ) ,
114
+ path . join ( process . cwd ( ) , 'node_modules' , '.bin' , 'browserstack-node-sdk' ) ,
115
+ // Fall back to letting the OS PATH resolve it (e.g., project-level node_modules/.bin)
116
+ 'browserstack-node-sdk'
117
+ ]
118
+ const sdkBin = sdkCandidates . find ( p => {
119
+ try {
120
+ // If candidate is a bare command (no path separators), let it pass
121
+ if ( ! p . includes ( path . sep ) ) return true
122
+ return fs . existsSync ( p )
123
+ } catch {
124
+ return false
125
+ }
126
+ } ) || 'browserstack-node-sdk'
127
+
128
+ if ( options . debug ) {
129
+ log . debug ( `Using BrowserStack SDK binary: ${ sdkBin } ` )
130
+ }
131
+
132
+ let testSuiteProcess
133
+ try {
134
+ testSuiteProcess = spawn . sync (
135
+ sdkBin ,
136
+ [ 'mocha' , '_generated' , '--timeouts' , String ( options . testTimeout ) , '-g' , options . filter , '--browserstack.config' , options . browserstackConfig , ...reporter ] ,
137
+ {
138
+ stdio : 'inherit' ,
139
+ env : {
140
+ ...process . env ,
141
+ testTimeout : options . testTimeout ,
142
+ NODE_PATH : `${ sideRunnerNodeModules } ${ path . delimiter } ${ process . env . NODE_PATH || '' } `
143
+ }
144
+ }
145
+ )
146
+ } catch ( err ) {
147
+ log . error ( `Failed to start BrowserStack SDK at "${ sdkBin } ": ${ err . message } ` )
148
+ exit ( 1 )
149
+ }
150
+
151
+ if ( testSuiteProcess . error ) {
152
+ log . error ( `Failed to start BrowserStack SDK at "${ sdkBin } ": ${ testSuiteProcess . error . message } ` )
153
+ exit ( 1 )
154
+ }
106
155
107
156
if ( ! options . debug ) {
108
157
rimrafSync ( options . buildFolderPath )
109
158
}
110
- exit ( testSuiteProcess . status )
159
+ exit ( testSuiteProcess . status ?? 1 )
0 commit comments