-
Notifications
You must be signed in to change notification settings - Fork 54
Keep the access key out of child argv; honour useCaCertificate without a proxy #180
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
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 |
|---|---|---|
|
|
@@ -34,7 +34,9 @@ function LocalBinary(){ | |
|
|
||
| let cmd, opts; | ||
| cmd = 'node'; | ||
| opts = [path.join(__dirname, 'fetchDownloadSourceUrl.js'), this.key, this.bsHost]; | ||
| /* The auth token is handed to the child through its environment, not argv — | ||
| argv is readable by any local user via `ps` / /proc/<pid>/cmdline. */ | ||
| opts = [path.join(__dirname, 'fetchDownloadSourceUrl.js'), this.bsHost]; | ||
|
|
||
| if (retries == 4 || (process.env.BINARY_DOWNLOAD_FALLBACK_ENABLED == 'true' && this.parentRetries == 4)) { | ||
| opts.push(true, this.downloadErrorMessage || process.env.BINARY_DOWNLOAD_ERROR_MESSAGE); | ||
|
|
@@ -53,6 +55,9 @@ function LocalBinary(){ | |
|
|
||
| const userAgent = [packageName, version].join('/'); | ||
| const env = Object.assign({ 'USER_AGENT': userAgent }, process.env); | ||
| if (this.key) { | ||
| env.BROWSERSTACK_LOCAL_AUTH_TOKEN = this.key; | ||
| } | ||
| const obj = childProcess.spawnSync(cmd, opts, { env: env }); | ||
| if(obj.stdout.length > 0) { | ||
| this.sourceURL = obj.stdout.toString().replace(/\n+$/, ''); | ||
|
|
@@ -135,10 +140,11 @@ function LocalBinary(){ | |
| var that = this; | ||
| if(retries > 0) { | ||
| console.log('Retrying Download. Retries left', retries); | ||
| fs.stat(binaryPath, function(err) { | ||
| if(err == null) { | ||
| fs.unlinkSync(binaryPath); | ||
| } | ||
| /* Single unlink instead of stat-then-unlinkSync: the gap between the two | ||
| let a concurrent writer swap the file, and a failing unlinkSync threw | ||
| out of the stat callback where it could not be caught. A missing file | ||
| is the expected case here, so any error is ignored. */ | ||
| fs.unlink(binaryPath, function() { | ||
| if(!callback) { | ||
| return that.downloadSync(conf, destParentDir, retries - 1); | ||
| } | ||
|
|
@@ -310,18 +316,38 @@ function LocalBinary(){ | |
| this.getAvailableDirs = function(){ | ||
| for(var i=0; i < this.orderedPaths.length; i++){ | ||
| var path = this.orderedPaths[i]; | ||
| if(this.makePath(path)) | ||
| // the last entry lives under the shared temp dir — it must be ours alone | ||
| var requirePrivate = (i === this.orderedPaths.length - 1); | ||
| if(this.makePath(path, requirePrivate)) | ||
| return path; | ||
| } | ||
| throw new LocalError('Error trying to download BrowserStack Local binary'); | ||
| }; | ||
|
|
||
| this.makePath = function(path){ | ||
| this.makePath = function(path, requirePrivate){ | ||
| try { | ||
| if(!this.checkPath(path)){ | ||
| fs.mkdirSync(path); | ||
| fs.mkdirSync(path, { mode: 0o700 }); | ||
|
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. [blocking] Evidence — the same
Why it matters: the pre-warm pattern — an image build or setup step downloads Fix — scope the mode to the path that is actually being hardened: if(!this.checkPath(path)){
fs.mkdirSync(path, requirePrivate ? { mode: 0o700 } : undefined);
}(Keeping 0700 everywhere is also defensible — but then the PR body and the completion comment both need to say so, and it should be called out as a behaviour change.) |
||
| } | ||
| return true; | ||
| return requirePrivate ? this.isUserPrivateDir(path) : true; | ||
| } catch(e){ | ||
| return false; | ||
| } | ||
| }; | ||
|
|
||
| /* Only applied to the shared-temp fallback. The binary is written there and | ||
| then executed, so that directory must not be writable by anyone but us — | ||
| otherwise another local user can swap the binary between the download and | ||
| the exec, or pre-create the path as a symlink. Windows has no POSIX mode | ||
| bits; there this is a no-op. */ | ||
| this.isUserPrivateDir = function(dirPath){ | ||
|
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. [blocking] Four of the five changes here are behavioural (argv→env token transport, The stated reason is that the repo's only suite is a credential-gated live-integration suite with no offline seam for these paths. That isn't quite right —
Only the two live-download checks need the network, and those are the ones worth skipping. There's no Ask: port those eleven checks into No test is expected for the Semgrep digest pin — config-only, and a version-pin assertion would be against convention. |
||
| if(process.platform === 'win32' || typeof process.getuid !== 'function') return true; | ||
| try { | ||
| var stats = fs.lstatSync(dirPath); | ||
| if(!stats.isDirectory()) return false; | ||
| if(stats.uid !== process.getuid()) return false; | ||
| // reject group- or world-writable | ||
| return (stats.mode & 0o022) === 0; | ||
| } catch(e){ | ||
| return false; | ||
| } | ||
|
|
@@ -349,10 +375,18 @@ function LocalBinary(){ | |
| return home || null; | ||
| }; | ||
|
|
||
| /* The last entry is a per-user subdirectory of the temp dir rather than the | ||
| temp dir itself: os.tmpdir() is /tmp on Linux, which is world-writable, and | ||
| the binary name below it is fixed and predictable. */ | ||
| this.tmpDirPath = function(){ | ||
| var suffix = (typeof process.getuid === 'function') ? String(process.getuid()) : 'user'; | ||
| return path.join(os.tmpdir(), 'browserstack-local-' + suffix); | ||
| }; | ||
|
|
||
| this.orderedPaths = [ | ||
| path.join(this.homedir(), '.browserstack'), | ||
| process.cwd(), | ||
| os.tmpdir() | ||
| this.tmpDirPath() | ||
| ]; | ||
| } | ||
|
|
||
|
|
||
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.
[nit] The move off argv is right, and I confirmed the position shift lines up exactly with the new
fetchDownloadSourceUrl.jsreads (bsHostargv[2] →downloadFallback[3] →downloadErrorMessage[4] →proxyHost[5] →proxyPort[6] →useCaCertificate[7]).One small thing: because
envis seeded fromprocess.envand the assignment is behindif (this.key), an ambientBROWSERSTACK_LOCAL_AUTH_TOKENin the parent's environment now flows through to the child wheneverthis.keyis falsy. Onmasterthat case sent the literal string"undefined"in argv, so it always failed cleanly; now it can silently authenticate with a value the caller never passed toLocal.start(). Unlikely to bite given the variable name, but it makes the child's auth non-deterministic w.r.t. the caller's own config.Also worth a line in the README: this is a public package, and the variable is now a de-facto input to it.