diff --git a/src/main/java/com/browserstack/local/LocalBinary.java b/src/main/java/com/browserstack/local/LocalBinary.java index 0d387a0..3f08bb3 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,39 @@ 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"); + } + 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 +271,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) {