@@ -8,6 +8,26 @@ const constants = require("./constants");
88const utils = require ( "./utils" ) ;
99const logger = require ( './logger' ) . winstonLogger ;
1010
11+ // Defense-in-depth: reject file paths containing shell metacharacters.
12+ // This guards against command injection even if execFileSync is ever
13+ // replaced with a shell-based exec in the future.
14+ //
15+ // Note: backslash (\) is intentionally NOT included here because it is a
16+ // legitimate path separator on Windows (e.g. C:\Users\me\cypress.config.js).
17+ // The actual security boundary is execFileSync (no shell), not this regex.
18+ const DANGEROUS_PATH_CHARS = / [ ; " ` $ | & ( ) { } ] / ;
19+
20+ function validateFilePath ( filepath ) {
21+ if ( DANGEROUS_PATH_CHARS . test ( filepath ) ) {
22+ throw new Error (
23+ `Invalid cypress config file path: "${ filepath } " contains disallowed characters. ` +
24+ 'File paths must not include shell metacharacters such as ; " ` $ | & ( ) { }'
25+ ) ;
26+ }
27+ }
28+
29+ exports . validateFilePath = validateFilePath ;
30+
1131exports . detectLanguage = ( cypress_config_filename ) => {
1232 const extension = cypress_config_filename . split ( '.' ) . pop ( )
1333 return constants . CYPRESS_V10_AND_ABOVE_CONFIG_FILE_EXTENSIONS . includes ( extension ) ? extension : 'js'
@@ -186,13 +206,29 @@ exports.convertTsConfig = (bsConfig, cypress_config_filepath, bstack_node_module
186206}
187207
188208exports . loadJsFile = ( cypress_config_filepath , bstack_node_modules_path ) => {
189- const require_module_helper_path = path . join ( __dirname , 'requireModule.js' )
190- let load_command = `NODE_PATH="${ bstack_node_modules_path } " node "${ require_module_helper_path } " "${ cypress_config_filepath } "`
191- if ( / ^ w i n / . test ( process . platform ) ) {
192- load_command = `set NODE_PATH=${ bstack_node_modules_path } && node "${ require_module_helper_path } " "${ cypress_config_filepath } "`
209+ // Security: validate file path to reject shell metacharacters (defense-in-depth)
210+ validateFilePath ( cypress_config_filepath ) ;
211+
212+ // UX: surface a clear error if the cypress config file is missing.
213+ // (This is purely a UX check — the security boundary is execFileSync above
214+ // plus the metacharacter regex; existsSync alone would NOT prevent injection.)
215+ if ( ! fs . existsSync ( cypress_config_filepath ) ) {
216+ throw new Error ( `Cypress config file not found at: ${ cypress_config_filepath } ` ) ;
193217 }
194- logger . debug ( `Running: ${ load_command } ` )
195- cp . execSync ( load_command )
218+
219+ const require_module_helper_path = path . join ( __dirname , 'requireModule.js' )
220+
221+ // Security fix: use execFileSync instead of execSync to avoid shell interpolation.
222+ // execFileSync spawns the process directly without a shell, so user-controlled
223+ // values in cypress_config_filepath cannot break out into shell commands.
224+ const execOptions = {
225+ env : Object . assign ( { } , process . env , { NODE_PATH : bstack_node_modules_path } )
226+ } ;
227+ const args = [ require_module_helper_path , cypress_config_filepath ] ;
228+
229+ logger . debug ( `Running: node ${ args . map ( a => '"' + a + '"' ) . join ( ' ' ) } (via execFileSync, NODE_PATH=${ bstack_node_modules_path } )` ) ;
230+ cp . execFileSync ( 'node' , args , execOptions ) ;
231+
196232 const cypress_config = JSON . parse ( fs . readFileSync ( config . configJsonFileName ) . toString ( ) )
197233 if ( fs . existsSync ( config . configJsonFileName ) ) {
198234 fs . unlinkSync ( config . configJsonFileName )
0 commit comments