-
Notifications
You must be signed in to change notification settings - Fork 44
[APS-18613] fix: prevent command injection via cypress_config_file in loadJsFile #1080
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 1 commit
6dbf8f9
9bb1f6f
d2d1c21
986f0d2
e9f1fbc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,22 @@ const constants = require("./constants"); | |
| const utils = require("./utils"); | ||
| const logger = require('./logger').winstonLogger; | ||
|
|
||
| // Defense-in-depth: reject file paths containing shell metacharacters. | ||
| // This guards against command injection even if execFileSync is ever | ||
| // replaced with a shell-based exec in the future. | ||
| const DANGEROUS_PATH_CHARS = /[;"`$|&(){}\\]/; | ||
|
|
||
| function validateFilePath(filepath) { | ||
| if (DANGEROUS_PATH_CHARS.test(filepath)) { | ||
| throw new Error( | ||
| `Invalid cypress config file path: "${filepath}" contains disallowed characters. ` + | ||
| 'File paths must not include shell metacharacters such as ; " ` $ | & ( ) { } \\' | ||
| ); | ||
| } | ||
| } | ||
|
|
||
| exports.validateFilePath = validateFilePath; | ||
|
|
||
| exports.detectLanguage = (cypress_config_filename) => { | ||
| const extension = cypress_config_filename.split('.').pop() | ||
| return constants.CYPRESS_V10_AND_ABOVE_CONFIG_FILE_EXTENSIONS.includes(extension) ? extension : 'js' | ||
|
|
@@ -186,13 +202,22 @@ exports.convertTsConfig = (bsConfig, cypress_config_filepath, bstack_node_module | |
| } | ||
|
|
||
| exports.loadJsFile = (cypress_config_filepath, bstack_node_modules_path) => { | ||
| // Security: validate file path to reject shell metacharacters (defense-in-depth) | ||
| validateFilePath(cypress_config_filepath); | ||
|
|
||
| const require_module_helper_path = path.join(__dirname, 'requireModule.js') | ||
| let load_command = `NODE_PATH="${bstack_node_modules_path}" node "${require_module_helper_path}" "${cypress_config_filepath}"` | ||
| if (/^win/.test(process.platform)) { | ||
| load_command = `set NODE_PATH=${bstack_node_modules_path}&& node "${require_module_helper_path}" "${cypress_config_filepath}"` | ||
| } | ||
| logger.debug(`Running: ${load_command}`) | ||
| cp.execSync(load_command) | ||
|
|
||
| // Security fix: use execFileSync instead of execSync to avoid shell interpolation. | ||
| // execFileSync spawns the process directly without a shell, so user-controlled | ||
| // values in cypress_config_filepath cannot break out into shell commands. | ||
| const execOptions = { | ||
| env: Object.assign({}, process.env, { NODE_PATH: bstack_node_modules_path }) | ||
| }; | ||
| const args = [require_module_helper_path, cypress_config_filepath]; | ||
|
|
||
| logger.debug(`Running: node ${args.map(a => '"' + a + '"').join(' ')} (via execFileSync, NODE_PATH=${bstack_node_modules_path})`); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How are we setting env vars?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Env vars are now passed through const execOptions = {
env: Object.assign({}, process.env, { NODE_PATH: bstack_node_modules_path })
};
const args = [require_module_helper_path, cypress_config_filepath];
cp.execFileSync('node', args, execOptions);Verified end-to-end with a shim that prints So the parent CLI process spawns
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Don't want to modify the existing logic, just validation should suffice.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wanted to lay out the rationale before reverting, because this isn't a stylistic refactor — it's the load-bearing security control:
The change is minimal and semantics-preserving: - cp.execSync(`NODE_PATH="${p}" node "${helper}" "${cfg}"`) // Unix
- cp.execSync(`set NODE_PATH=${p}&& node "${helper}" "${cfg}"`) // Windows
+ cp.execFileSync('node', [helper, cfg], { env: {...process.env, NODE_PATH: p} })Same Confidence — what we've validated directly on
Where my confidence isn't 100%:
Suggested path: run this PR through the QA If after the regression you still prefer regex-only, I'll revert to |
||
| cp.execFileSync('node', args, execOptions); | ||
|
|
||
| const cypress_config = JSON.parse(fs.readFileSync(config.configJsonFileName).toString()) | ||
| if (fs.existsSync(config.configJsonFileName)) { | ||
| fs.unlinkSync(config.configJsonFileName) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
\ is valid for windows right? Not alligned with this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why can't we just check if the path exists or not?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch — you're right,
\is a valid path separator on Windows. I've removed\fromDANGEROUS_PATH_CHARSin commit 9bb1f6f.This is safe because the actual security boundary here is
execFileSync(no shell invocation), not the regex. Once you spawnnodedirectly without a shell,\carries no shell meaning anywhere. The regex is purely defense-in-depth against accidental future regressions toexecSync/exec. So we only need to reject characters that have shell meaning on Unix or Windows shells (; "$ | & ( ) { }`) — not legitimate path separators.I added Windows-path positive tests to lock this in:
C:\Users\test\cypress.config.js✓C:\Program Files\my app\cypress.config.js✓ (spaces + backslashes).\subdir\cypress.config.js✓\\server\share\cypress.config.js✓ (UNC)Validated end-to-end with a real BrowserStack production session on Windows 11: build
f5ca218e318ca84b90189b55440fef7edad300b5— 1/1 passed.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Existence-checking by itself is not a security control here — it would tell us if the file currently exists, but it wouldn't stop a path like
cypress.config.js"; rm -rf ~from being interpolated into a shell command. The attacker doesn't need the literal file to exist for the injected suffix (; rm -rf ~) to run — once the string reachesexecSync, the shell parses it regardless of whethercypress.config.js"is a real file.Demonstrated this experimentally on master (commit 9bb1f6f workspace): with the master tarball, payload
nonexistent.config.js"; touch /tmp/aps18613-master-pwn-<ts>; echo "created the marker file even thoughnonexistent.config.jsdoesn't exist. With the fix tarball, the same payload was rejected byvalidateFilePathbefore reachingexecFileSync— no marker file created.That said —
existsSyncIS valuable as a UX improvement (clearer error message), so I added it in 9bb1f6f insideloadJsFile, aftervalidateFilePathand beforeexecFileSync. It now throwsCypress config file not found at: <path>if the file is missing, with an inline comment documenting that this is for UX, not security. The security guarantees remainexecFileSync(no shell) + the metacharacter regex.