Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion src/main/java/com/browserstack/local/LocalBinary.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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 {
Comment thread
rounak610 marked this conversation as resolved.
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;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you had to do this all along then why all the checks before this?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each check covers a different case the host equals on line 67 doesn't:

  • HTTPS check (line 58): allowlist matches host only, so without this http://browserstack.com would still pass and we'd download over plain HTTP.
  • MalformedURLException catch (line 55): converts a raw JVM exception into LocalException so the caller sees one exception type.
  • null/empty host check (line 62): getHost() returns null for URLs like https:///foo. Without this, host.toLowerCase() on line 65 throws NPE.
  • null URL check (line 49): new URL(null) throws NPE before the catch can run.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ack got it, mind adding a small comment at the top to make sure the next dev understands the purpose?
(e.g my instinct was directly to de-dup code, didn't realise throwing specific exceptions was the use case)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}
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();
Expand Down Expand Up @@ -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"));
Comment thread
rounak610 marked this conversation as resolved.
if(fallbackEnabled) downloadFailureThrowable = null;
}
} catch (Throwable e) {
Expand Down
Loading