From c0e8306c14b9d2c22ca05e187c3dc473d66f533c Mon Sep 17 00:00:00 2001 From: rounak bhatia Date: Tue, 9 Jun 2026 20:24:01 +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/browserstack/fetch_download_source_url.rb | 25 ++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/lib/browserstack/fetch_download_source_url.rb b/lib/browserstack/fetch_download_source_url.rb index 34fbde0..37fe977 100644 --- a/lib/browserstack/fetch_download_source_url.rb +++ b/lib/browserstack/fetch_download_source_url.rb @@ -8,6 +8,29 @@ module BrowserStack module FetchDownloadSourceUrl BS_HOST = 'local.browserstack.com'.freeze ENDPOINT_PATH = '/binary/api/v1/endpoint'.freeze + ALLOWED_DOWNLOAD_HOSTS = ['browserstack.com'].freeze + ALLOWED_DOWNLOAD_HOST_SUFFIXES = ['.browserstack.com'].freeze + + def self.validate_source_url(url) + if url.nil? || url.to_s.empty? + raise BrowserStack::LocalException.new('Refusing binary download: empty source URL') + end + uri = begin + URI.parse(url) + rescue URI::InvalidURIError + raise BrowserStack::LocalException.new('Refusing binary download: malformed source URL') + end + unless uri.scheme == 'https' + raise BrowserStack::LocalException.new('Refusing binary download from non-HTTPS source URL') + end + host = (uri.host || '').downcase + if host.empty? + raise BrowserStack::LocalException.new('Refusing binary download: source URL has no host') + end + return url if ALLOWED_DOWNLOAD_HOSTS.include?(host) + return url if ALLOWED_DOWNLOAD_HOST_SUFFIXES.any? { |suffix| host.end_with?(suffix) } + raise BrowserStack::LocalException.new("Refusing binary download: host '#{host}' is not in the allowed host list") + end def self.call(auth_token:, user_agent:, fallback: false, error_message: nil, proxy_host: nil, proxy_port: nil) @@ -57,7 +80,7 @@ def self.call(auth_token:, user_agent:, fallback: false, error_message: nil, ) end - endpoint + validate_source_url(endpoint) end end end From 4357ee9e4b2d1b5663aba9337ac61e70152aa2a3 Mon Sep 17 00:00:00 2001 From: rounak bhatia Date: Wed, 10 Jun 2026 13:34:27 +0530 Subject: [PATCH 2/2] Add comment explaining each guard in validate_source_url Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/browserstack/fetch_download_source_url.rb | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/browserstack/fetch_download_source_url.rb b/lib/browserstack/fetch_download_source_url.rb index 37fe977..998fef7 100644 --- a/lib/browserstack/fetch_download_source_url.rb +++ b/lib/browserstack/fetch_download_source_url.rb @@ -11,6 +11,11 @@ module FetchDownloadSourceUrl ALLOWED_DOWNLOAD_HOSTS = ['browserstack.com'].freeze ALLOWED_DOWNLOAD_HOST_SUFFIXES = ['.browserstack.com'].freeze + # Each guard below covers a case the final host-equals check does not: + # - nil/empty URL: URI.parse(nil) raises TypeError before the rescue can catch it. + # - URI::InvalidURIError: convert raw Ruby error into BrowserStack::LocalException for the public contract. + # - HTTPS check: allowlist matches host only; without this, http://browserstack.com would pass. + # - nil/empty host: uri.host is nil for URIs like https:///foo, which would crash on downcase. def self.validate_source_url(url) if url.nil? || url.to_s.empty? raise BrowserStack::LocalException.new('Refusing binary download: empty source URL')