From 5a014718c897c8be8618ef8a9ccbf2ea25d00f3d Mon Sep 17 00:00:00 2001 From: rounak bhatia Date: Thu, 4 Jun 2026 19:21:25 +0530 Subject: [PATCH 1/2] added host check for binary download url --- .../com/browserstack/local/LocalBinary.java | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/browserstack/local/LocalBinary.java b/src/main/java/com/browserstack/local/LocalBinary.java index b7abd79..bc48b36 100644 --- a/src/main/java/com/browserstack/local/LocalBinary.java +++ b/src/main/java/com/browserstack/local/LocalBinary.java @@ -22,6 +22,9 @@ class LocalBinary { + private static final String[] ALLOWED_DOWNLOAD_HOSTS = { "browserstack.com" }; + private static final String[] ALLOWED_DOWNLOAD_HOST_SUFFIXES = { ".browserstack.com" }; + private String binaryFileName; private String sourceUrl; @@ -42,6 +45,34 @@ class LocalBinary { System.getProperty("java.io.tmpdir") }; + private static String validateSourceUrl(String url) throws LocalException { + if (url == null || url.isEmpty()) { + throw new LocalException("Refusing binary download: empty source URL"); + } + URL parsed; + try { + parsed = new URL(url); + } catch (java.net.MalformedURLException e) { + throw new LocalException("Refusing binary download: malformed source URL"); + } + if (!"https".equalsIgnoreCase(parsed.getProtocol())) { + throw new LocalException("Refusing binary download from non-HTTPS source URL"); + } + String host = parsed.getHost(); + if (host == null || host.isEmpty()) { + throw new LocalException("Refusing binary download: source URL has no host"); + } + host = host.toLowerCase(); + for (String allowed : ALLOWED_DOWNLOAD_HOSTS) { + if (host.equals(allowed)) return url; + } + for (String suffix : ALLOWED_DOWNLOAD_HOST_SUFFIXES) { + if (host.endsWith(suffix)) return url; + } + throw new LocalException( + "Refusing binary download: host '" + host + "' is not in the allowed host list"); + } + LocalBinary(String path, String key) throws LocalException { this.key = key; initialize(); @@ -235,7 +266,7 @@ private void fetchSourceUrl() throws LocalException { if (json.has("error")) { throw new Exception(json.getString("error")); } - this.sourceUrl = json.getJSONObject("data").getString("endpoint"); + this.sourceUrl = validateSourceUrl(json.getJSONObject("data").getString("endpoint")); if(fallbackEnabled) downloadFailureThrowable = null; } } catch (Throwable e) { From 5f1e535a95780a8dafdbb33bd3865cbaabb0a36a Mon Sep 17 00:00:00 2001 From: rounak bhatia Date: Tue, 9 Jun 2026 22:31:48 +0530 Subject: [PATCH 2/2] Add comment explaining each guard in validateSourceUrl Co-Authored-By: Claude Opus 4.7 (1M context) --- src/main/java/com/browserstack/local/LocalBinary.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/com/browserstack/local/LocalBinary.java b/src/main/java/com/browserstack/local/LocalBinary.java index 00de4e1..3f08bb3 100644 --- a/src/main/java/com/browserstack/local/LocalBinary.java +++ b/src/main/java/com/browserstack/local/LocalBinary.java @@ -45,6 +45,11 @@ class LocalBinary { System.getProperty("java.io.tmpdir") }; + // Each guard below covers a case the final host-equals check does not: + // - null/empty URL: new URL(null) throws NPE before the catch can run. + // - MalformedURLException: convert raw JVM exception to LocalException for the public contract. + // - HTTPS check: allowlist matches host only; without this, http://browserstack.com would pass. + // - null/empty host: getHost() returns null for URLs like https:///foo, which NPEs on toLowerCase(). private static String validateSourceUrl(String url) throws LocalException { if (url == null || url.isEmpty()) { throw new LocalException("Refusing binary download: empty source URL");