Skip to content

Commit 9eecf6c

Browse files
authored
Master merge
2 parents 5a34e89 + 40517f3 commit 9eecf6c

6 files changed

Lines changed: 1996 additions & 3704 deletions

File tree

bin/helpers/readCypressConfigUtil.js

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,26 @@ const constants = require("./constants");
88
const utils = require("./utils");
99
const 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+
1131
exports.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

188208
exports.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 (/^win/.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)

bin/testObservability/cypress/index.js

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -201,24 +201,30 @@ Cypress.on('command:end', (command) => {
201201
});
202202
});
203203

204-
Cypress.Commands.overwrite('log', (originalFn, ...args) => {
204+
Cypress.on('command:enqueued', (attrs) => {
205+
if (!Cypress.env('BROWSERSTACK_O11Y_LOGS')) return;
206+
if (!attrs || attrs.name !== 'log') return;
207+
const args = attrs.args || [];
205208
if (args.includes('test_observability_log') || args.includes('test_observability_command')) return;
206209
const message = args.reduce((result, logItem) => {
207210
if (typeof logItem === 'object') {
208211
return [result, JSON.stringify(logItem)].join(' ');
209212
}
210-
211213
return [result, logItem ? logItem.toString() : ''].join(' ');
212214
}, '');
213215
eventsQueue.push({
214216
task: 'test_observability_log',
215217
data: {
216-
'level': 'info',
218+
level: 'info',
217219
message,
218220
timestamp: new Date().toISOString()
219221
},
220222
options: { log: false }
221223
});
224+
});
225+
226+
Cypress.Commands.overwrite('log', (originalFn, ...args) => {
227+
if (args.includes('test_observability_log') || args.includes('test_observability_command')) return;
222228
originalFn(...args);
223229
});
224230

bin/testObservability/reporter/index.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -631,7 +631,9 @@ class MyReporter {
631631
if(this.current_hook && ( this.current_hook.hookAnalyticsId && !this.runStatusMarkedHash[this.current_hook.hookAnalyticsId] )) {
632632
log.hook_run_uuid = this.current_hook.hookAnalyticsId;
633633
}
634-
if(!log.hook_run_uuid && this.current_test && ( this.current_test.testAnalyticsId && !this.runStatusMarkedHash[this.current_test.testAnalyticsId] )) log.test_run_uuid = this.current_test.testAnalyticsId;
634+
if(!log.hook_run_uuid && this.current_test && this.current_test.testAnalyticsId) {
635+
log.test_run_uuid = this.current_test.testAnalyticsId;
636+
}
635637
if(log.hook_run_uuid || log.test_run_uuid) {
636638
await uploadEventData({
637639
event_type: 'LogCreated',

0 commit comments

Comments
 (0)