From 7b57e7b0abdef85a43af17d4308445275591c058 Mon Sep 17 00:00:00 2001 From: rounak bhatia Date: Tue, 9 Jun 2026 20:23:52 +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) --- .../BrowserStackLocal/BrowserStackTunnel.cs | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/BrowserStackLocal/BrowserStackLocal/BrowserStackTunnel.cs b/BrowserStackLocal/BrowserStackLocal/BrowserStackTunnel.cs index f825c29..e24c0d0 100644 --- a/BrowserStackLocal/BrowserStackLocal/BrowserStackTunnel.cs +++ b/BrowserStackLocal/BrowserStackLocal/BrowserStackTunnel.cs @@ -21,6 +21,9 @@ public enum LocalState { Idle, Connecting, Connected, Error, Disconnected }; public class BrowserStackTunnel : IDisposable { + private static readonly string[] AllowedDownloadHosts = new string[] { "browserstack.com" }; + private static readonly string[] AllowedDownloadHostSuffixes = new string[] { ".browserstack.com" }; + static readonly string uname = Util.GetUName(); static readonly string binaryName = GetBinaryName(); @@ -229,11 +232,43 @@ private string fetchSourceUrl(string accessKey) throw new Exception((string)jsonResponse["error"]); } - sourceUrl = jsonResponse["data"]?["endpoint"]?.ToString(); + sourceUrl = ValidateSourceUrl(jsonResponse["data"]?["endpoint"]?.ToString()); return sourceUrl; } } + private static string ValidateSourceUrl(string url) + { + if (string.IsNullOrEmpty(url)) + { + throw new Exception("Refusing binary download: empty source URL"); + } + Uri parsed; + if (!Uri.TryCreate(url, UriKind.Absolute, out parsed)) + { + throw new Exception("Refusing binary download: malformed source URL"); + } + if (!string.Equals(parsed.Scheme, "https", StringComparison.OrdinalIgnoreCase)) + { + throw new Exception("Refusing binary download from non-HTTPS source URL"); + } + string host = parsed.Host; + if (string.IsNullOrEmpty(host)) + { + throw new Exception("Refusing binary download: source URL has no host"); + } + host = host.ToLowerInvariant(); + foreach (var allowed in AllowedDownloadHosts) + { + if (host.Equals(allowed)) return url; + } + foreach (var suffix in AllowedDownloadHostSuffixes) + { + if (host.EndsWith(suffix)) return url; + } + throw new Exception("Refusing binary download: host '" + host + "' is not in the allowed host list"); + } + public void downloadBinary() { string binaryDirectory = Path.Combine(this.binaryAbsolute, ".."); From c9bfb64c9087a556925d72079b02a7f122e7fc41 Mon Sep 17 00:00:00 2001 From: rounak bhatia Date: Wed, 10 Jun 2026 13:34:10 +0530 Subject: [PATCH 2/2] Add comment explaining each guard in ValidateSourceUrl Co-Authored-By: Claude Opus 4.7 (1M context) --- BrowserStackLocal/BrowserStackLocal/BrowserStackTunnel.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/BrowserStackLocal/BrowserStackLocal/BrowserStackTunnel.cs b/BrowserStackLocal/BrowserStackLocal/BrowserStackTunnel.cs index e24c0d0..ce109a0 100644 --- a/BrowserStackLocal/BrowserStackLocal/BrowserStackTunnel.cs +++ b/BrowserStackLocal/BrowserStackLocal/BrowserStackTunnel.cs @@ -237,6 +237,11 @@ private string fetchSourceUrl(string accessKey) } } + // Each guard below covers a case the final host-equals check does not: + // - null/empty URL: skip a parse attempt and give a clear error. + // - Uri.TryCreate failure: malformed URL surfaces our own exception instead of leaving parsed null. + // - HTTPS check: allowlist matches host only; without this, http://browserstack.com would pass. + // - null/empty host: parsed.Host can be empty for some URL forms; give a clear error before reaching the allowlist loop. private static string ValidateSourceUrl(string url) { if (string.IsNullOrEmpty(url))