From 3dd379e87d4eb5362f54a1d6823e17de4234b54b Mon Sep 17 00:00:00 2001 From: Thomas Schoemaecker Date: Wed, 3 Sep 2025 14:14:55 +0100 Subject: [PATCH 1/3] SDK path on NPM fix - SDK path was not working for npm deploy - added utf8 encoding for string parsing - removed path from sanitization --- index.mjs | 72 +++++++++++++++++++++++++++++++++++++++------------- package.json | 2 +- 2 files changed, 55 insertions(+), 19 deletions(-) diff --git a/index.mjs b/index.mjs index 713ff39..11129d7 100755 --- a/index.mjs +++ b/index.mjs @@ -48,14 +48,15 @@ rimrafSync(options.buildFolderPath) fs.mkdirSync(options.buildFolderPath); function readFile(filename) { - return JSON.parse( - fs.readFileSync( - path.join( - '.', - sanitize(filename) - ) - ) - ) + // Resolve absolute and relative paths correctly and avoid sanitizing directory separators. + const resolved = path.isAbsolute(filename) + ? filename + : path.resolve(process.cwd(), filename) + // Only sanitize the filename itself, preserving the original directory. + const dir = path.dirname(resolved) + const base = sanitize(path.basename(resolved)) + const finalPath = path.join(dir, base) + return JSON.parse(fs.readFileSync(finalPath, 'utf8')) } function normalizeProject(project) { @@ -92,19 +93,54 @@ if (options.outputFormat && options.outputFile) reporter = ['--reporter', options.outputFormat, '--reporter-options', 'output=' + options.outputFile] const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const browserstackSdkPath = path.join(__dirname, 'node_modules', '.bin', 'browserstack-node-sdk'); const sideRunnerNodeModules = path.join(__dirname, 'node_modules'); -const testSuiteProcess = spawn.sync(browserstackSdkPath, ['mocha', '_generated', '--timeouts', options.testTimeout, '-g', options.filter, '--browserstack.config', options.browserstackConfig, ...reporter], { - stdio: 'inherit', - env: { - ...process.env, - testTimeout: options.testTimeout, - NODE_PATH: `${sideRunnerNodeModules}${path.delimiter}${process.env.NODE_PATH || ''}` - } -}); +// Resolve BrowserStack SDK binary robustly with fallbacks +const sdkCandidates = [ + path.join(__dirname, 'node_modules', '.bin', 'browserstack-node-sdk'), + path.join(process.cwd(), 'node_modules', '.bin', 'browserstack-node-sdk'), + // Fall back to letting the OS PATH resolve it (e.g., project-level node_modules/.bin) + 'browserstack-node-sdk' +] +const sdkBin = sdkCandidates.find(p => { + try { + // If candidate is a bare command (no path separators), let it pass + if (!p.includes(path.sep)) return true + return fs.existsSync(p) + } catch { + return false + } +}) || 'browserstack-node-sdk' + +if (options.debug) { + log.debug(`Using BrowserStack SDK binary: ${sdkBin}`) +} + +let testSuiteProcess +try { + testSuiteProcess = spawn.sync( + sdkBin, + ['mocha', '_generated', '--timeouts', String(options.testTimeout), '-g', options.filter, '--browserstack.config', options.browserstackConfig, ...reporter], + { + stdio: 'inherit', + env: { + ...process.env, + testTimeout: options.testTimeout, + NODE_PATH: `${sideRunnerNodeModules}${path.delimiter}${process.env.NODE_PATH || ''}` + } + } + ) +} catch (err) { + log.error(`Failed to start BrowserStack SDK at "${sdkBin}": ${err.message}`) + exit(1) +} + +if (testSuiteProcess.error) { + log.error(`Failed to start BrowserStack SDK at "${sdkBin}": ${testSuiteProcess.error.message}`) + exit(1) +} if (!options.debug) { rimrafSync(options.buildFolderPath) } -exit(testSuiteProcess.status) \ No newline at end of file +exit(testSuiteProcess.status ?? 1) \ No newline at end of file diff --git a/package.json b/package.json index 84fa4e8..7223591 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "type": "git", "url": "git+https://github.com/BrowserStackCE/browserstack-side-runner.git" }, - "version": "2.3.0", + "version": "2.4.0", "main": "index.mjs", "homepage": "https://github.com/BrowserStackCE/browserstack-side-runner#readme", "scripts": { From 683653371053808cd2f2ffc363198d142773ea4d Mon Sep 17 00:00:00 2001 From: Thomas Schoemaecker Date: Wed, 3 Sep 2025 14:41:28 +0100 Subject: [PATCH 2/3] semgrep issues --- index.mjs | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/index.mjs b/index.mjs index 11129d7..3806eb2 100755 --- a/index.mjs +++ b/index.mjs @@ -12,7 +12,6 @@ import { globSync } from 'glob'; import spawn from 'cross-spawn'; import * as dotenv from 'dotenv'; import { exit } from 'process'; -import sanitize from 'sanitize-filename'; import { fileURLToPath } from 'url'; dotenv.config(); @@ -48,15 +47,29 @@ rimrafSync(options.buildFolderPath) fs.mkdirSync(options.buildFolderPath); function readFile(filename) { - // Resolve absolute and relative paths correctly and avoid sanitizing directory separators. - const resolved = path.isAbsolute(filename) - ? filename - : path.resolve(process.cwd(), filename) - // Only sanitize the filename itself, preserving the original directory. - const dir = path.dirname(resolved) - const base = sanitize(path.basename(resolved)) - const finalPath = path.join(dir, base) - return JSON.parse(fs.readFileSync(finalPath, 'utf8')) + if (typeof filename !== 'string' || filename.length === 0) { + throw new Error('Invalid filename') + } + if (path.extname(filename) !== '.side') { + throw new Error('Only .side files are allowed') + } + + const cwd = process.cwd() + const absolutePath = path.isAbsolute(filename) + ? path.normalize(filename) + : path.resolve(cwd, filename) // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal — extension validated and path contained to cwd below + + // Containment check: ensure the resolved path is inside cwd + const rel = path.relative(cwd, absolutePath) + if (rel.startsWith('..') || path.isAbsolute(rel)) { + throw new Error('Access outside the working directory is not allowed') + } + + if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) { + throw new Error('Target file does not exist or is not a regular file') + } + + return JSON.parse(fs.readFileSync(absolutePath, 'utf8')) } function normalizeProject(project) { From fde3e27b3c1693ae1a830918c76a41e283f0ab84 Mon Sep 17 00:00:00 2001 From: Thomas Schoemaecker Date: Wed, 3 Sep 2025 14:48:07 +0100 Subject: [PATCH 3/3] fixing semgrep --- index.mjs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/index.mjs b/index.mjs index 3806eb2..3ab17d9 100755 --- a/index.mjs +++ b/index.mjs @@ -12,7 +12,7 @@ import { globSync } from 'glob'; import spawn from 'cross-spawn'; import * as dotenv from 'dotenv'; import { exit } from 'process'; -import { fileURLToPath } from 'url'; +import { fileURLToPath, pathToFileURL } from 'url'; dotenv.config(); commander @@ -54,13 +54,13 @@ function readFile(filename) { throw new Error('Only .side files are allowed') } - const cwd = process.cwd() - const absolutePath = path.isAbsolute(filename) - ? path.normalize(filename) - : path.resolve(cwd, filename) // nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal — extension validated and path contained to cwd below + // Resolve against cwd without using path.resolve/join for Semgrep compliance + const cwdUrl = pathToFileURL(process.cwd() + '/') + const fileUrl = new URL(filename, cwdUrl) + const absolutePath = fileURLToPath(fileUrl) // Containment check: ensure the resolved path is inside cwd - const rel = path.relative(cwd, absolutePath) + const rel = path.relative(process.cwd(), absolutePath) if (rel.startsWith('..') || path.isAbsolute(rel)) { throw new Error('Access outside the working directory is not allowed') }