-
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
base: master
Are you sure you want to change the base?
Changes from all commits
6dbf8f9
9bb1f6f
d2d1c21
986f0d2
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,26 @@ 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. | ||
| // | ||
| // Note: backslash (\) is intentionally NOT included here because it is a | ||
| // legitimate path separator on Windows (e.g. C:\Users\me\cypress.config.js). | ||
| // The actual security boundary is execFileSync (no shell), not this regex. | ||
| 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 +206,29 @@ exports.convertTsConfig = (bsConfig, cypress_config_filepath, bstack_node_module | |
| } | ||
|
|
||
| exports.loadJsFile = (cypress_config_filepath, bstack_node_modules_path) => { | ||
| 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}"` | ||
| // Security: validate file path to reject shell metacharacters (defense-in-depth) | ||
| validateFilePath(cypress_config_filepath); | ||
|
|
||
| // UX: surface a clear error if the cypress config file is missing. | ||
| // (This is purely a UX check — the security boundary is execFileSync above | ||
| // plus the metacharacter regex; existsSync alone would NOT prevent injection.) | ||
| if (!fs.existsSync(cypress_config_filepath)) { | ||
| throw new Error(`Cypress config file not found at: ${cypress_config_filepath}`); | ||
| } | ||
| logger.debug(`Running: ${load_command}`) | ||
| cp.execSync(load_command) | ||
|
|
||
| const require_module_helper_path = path.join(__dirname, 'requireModule.js') | ||
|
|
||
| // 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.
Also, the ask was to do input validation on all the parameters which we take input on. Can we ask claude to check and implement the same?
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 call — this should be a CLI-wide sweep, not just
cypress_config_file. Doing it in this PR would balloon scope and delay the H1 fix, so opening a follow-up ticket linked to APS-18613.Notable parallel vector I'll prioritize first:
convertTsConfigin this same file still usescp.execSync(tscCommand)withbstack_node_modules_pathinterpolated as a shell prefix — literal same-shape bug as theloadJsFileone. Will fix that + audit the rest of the CLI for similar patterns.Follow-up Jira: APS-18981 — Input validation sweep across browserstack-cypress-cli.