From 8936933b70edcf91acc2cadb77f2ca910f966559 Mon Sep 17 00:00:00 2001 From: rounak bhatia Date: Tue, 9 Jun 2026 20:23:56 +0530 Subject: [PATCH 1/2] LOC-6727: validate source URL host before binary download Mirror the host allowlist added in the Java and Python bindings (browserstack-local-java#99, browserstack-local-python#62). Refuse download endpoints that aren't HTTPS or whose host isn't browserstack.com / *.browserstack.com. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/fetchDownloadSourceUrl.js | 4 ++-- lib/fetchDownloadSourceUrlAsync.js | 4 ++-- lib/util.js | 29 +++++++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/lib/fetchDownloadSourceUrl.js b/lib/fetchDownloadSourceUrl.js index df5c8f2..333d647 100644 --- a/lib/fetchDownloadSourceUrl.js +++ b/lib/fetchDownloadSourceUrl.js @@ -1,7 +1,7 @@ const https = require('https'), fs = require('fs'), HttpsProxyAgent = require('https-proxy-agent'), - { isUndefined } = require('./util'); + { isUndefined, validateSourceUrl } = require('./util'); const authToken = process.argv[2], bsHost = process.argv[3], proxyHost = process.argv[6], proxyPort = process.argv[7], useCaCertificate = process.argv[8], downloadFallback = process.argv[4], downloadErrorMessage = process.argv[5]; @@ -45,7 +45,7 @@ const req = https.request(options, res => { if(reqBody.error) { throw reqBody.error; } - console.log(reqBody.data.endpoint); + console.log(validateSourceUrl(reqBody.data.endpoint)); } catch (e) { console.error(e); } diff --git a/lib/fetchDownloadSourceUrlAsync.js b/lib/fetchDownloadSourceUrlAsync.js index ad6e97a..7e6c994 100644 --- a/lib/fetchDownloadSourceUrlAsync.js +++ b/lib/fetchDownloadSourceUrlAsync.js @@ -1,7 +1,7 @@ const https = require('https'), fs = require('fs'), HttpsProxyAgent = require('https-proxy-agent'), - { isUndefined } = require('./util'), + { isUndefined, validateSourceUrl } = require('./util'), version = require('../package.json').version; const packageName = 'browserstack-local-nodejs'; @@ -48,7 +48,7 @@ function fetchDownloadSourceUrlAsync(authToken, bsHost, downloadFallback, downlo if(reqBody.error) { throw reqBody.error; } - callback(null, reqBody.data.endpoint); + callback(null, validateSourceUrl(reqBody.data.endpoint)); } catch (e) { console.error(e); callback(e); diff --git a/lib/util.js b/lib/util.js index b7de5b5..9db36fe 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1 +1,30 @@ +const url = require('url'); + module.exports.isUndefined = value => (value === undefined || value === null || value === 'undefined'); + +const ALLOWED_DOWNLOAD_HOSTS = ['browserstack.com']; +const ALLOWED_DOWNLOAD_HOST_SUFFIXES = ['.browserstack.com']; + +module.exports.validateSourceUrl = function(sourceUrl) { + if (!sourceUrl || typeof sourceUrl !== 'string') { + throw new Error('Refusing binary download: empty source URL'); + } + let parsed; + try { + parsed = new url.URL(sourceUrl); + } catch (e) { + throw new Error('Refusing binary download: malformed source URL'); + } + if (parsed.protocol !== 'https:') { + throw new Error('Refusing binary download from non-HTTPS source URL'); + } + const host = (parsed.hostname || '').toLowerCase(); + if (!host) { + throw new Error('Refusing binary download: source URL has no host'); + } + if (ALLOWED_DOWNLOAD_HOSTS.indexOf(host) !== -1) return sourceUrl; + for (const suffix of ALLOWED_DOWNLOAD_HOST_SUFFIXES) { + if (host.endsWith(suffix)) return sourceUrl; + } + throw new Error("Refusing binary download: host '" + host + "' is not in the allowed host list"); +}; From e9d41cac623a2ba9a677f736ad34202d24118d00 Mon Sep 17 00:00:00 2001 From: rounak bhatia Date: Wed, 10 Jun 2026 13:34:19 +0530 Subject: [PATCH 2/2] Add comment explaining each guard in validateSourceUrl Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/util.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/util.js b/lib/util.js index 9db36fe..732ff6b 100644 --- a/lib/util.js +++ b/lib/util.js @@ -5,6 +5,11 @@ module.exports.isUndefined = value => (value === undefined || value === null || const ALLOWED_DOWNLOAD_HOSTS = ['browserstack.com']; const ALLOWED_DOWNLOAD_HOST_SUFFIXES = ['.browserstack.com']; +// Each guard below covers a case the final host-equals check does not: +// - empty/non-string URL: new url.URL(null) throws TypeError; explicit guard returns a clean message. +// - URL constructor catch: convert TypeError on malformed input into our own Error. +// - HTTPS check: allowlist matches host only; without this, http://browserstack.com would pass. +// - null/empty hostname: URL constructor accepts forms like https:///foo where hostname is empty; give a clear error. module.exports.validateSourceUrl = function(sourceUrl) { if (!sourceUrl || typeof sourceUrl !== 'string') { throw new Error('Refusing binary download: empty source URL');