Skip to content

Commit 8936933

Browse files
rounak610claude
andcommitted
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) <noreply@anthropic.com>
1 parent fa2dcbc commit 8936933

3 files changed

Lines changed: 33 additions & 4 deletions

File tree

lib/fetchDownloadSourceUrl.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const https = require('https'),
22
fs = require('fs'),
33
HttpsProxyAgent = require('https-proxy-agent'),
4-
{ isUndefined } = require('./util');
4+
{ isUndefined, validateSourceUrl } = require('./util');
55

66
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];
77

@@ -45,7 +45,7 @@ const req = https.request(options, res => {
4545
if(reqBody.error) {
4646
throw reqBody.error;
4747
}
48-
console.log(reqBody.data.endpoint);
48+
console.log(validateSourceUrl(reqBody.data.endpoint));
4949
} catch (e) {
5050
console.error(e);
5151
}

lib/fetchDownloadSourceUrlAsync.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const https = require('https'),
22
fs = require('fs'),
33
HttpsProxyAgent = require('https-proxy-agent'),
4-
{ isUndefined } = require('./util'),
4+
{ isUndefined, validateSourceUrl } = require('./util'),
55
version = require('../package.json').version;
66

77
const packageName = 'browserstack-local-nodejs';
@@ -48,7 +48,7 @@ function fetchDownloadSourceUrlAsync(authToken, bsHost, downloadFallback, downlo
4848
if(reqBody.error) {
4949
throw reqBody.error;
5050
}
51-
callback(null, reqBody.data.endpoint);
51+
callback(null, validateSourceUrl(reqBody.data.endpoint));
5252
} catch (e) {
5353
console.error(e);
5454
callback(e);

lib/util.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,30 @@
1+
const url = require('url');
2+
13
module.exports.isUndefined = value => (value === undefined || value === null || value === 'undefined');
4+
5+
const ALLOWED_DOWNLOAD_HOSTS = ['browserstack.com'];
6+
const ALLOWED_DOWNLOAD_HOST_SUFFIXES = ['.browserstack.com'];
7+
8+
module.exports.validateSourceUrl = function(sourceUrl) {
9+
if (!sourceUrl || typeof sourceUrl !== 'string') {
10+
throw new Error('Refusing binary download: empty source URL');
11+
}
12+
let parsed;
13+
try {
14+
parsed = new url.URL(sourceUrl);
15+
} catch (e) {
16+
throw new Error('Refusing binary download: malformed source URL');
17+
}
18+
if (parsed.protocol !== 'https:') {
19+
throw new Error('Refusing binary download from non-HTTPS source URL');
20+
}
21+
const host = (parsed.hostname || '').toLowerCase();
22+
if (!host) {
23+
throw new Error('Refusing binary download: source URL has no host');
24+
}
25+
if (ALLOWED_DOWNLOAD_HOSTS.indexOf(host) !== -1) return sourceUrl;
26+
for (const suffix of ALLOWED_DOWNLOAD_HOST_SUFFIXES) {
27+
if (host.endsWith(suffix)) return sourceUrl;
28+
}
29+
throw new Error("Refusing binary download: host '" + host + "' is not in the allowed host list");
30+
};

0 commit comments

Comments
 (0)