diff --git a/BrowserStackLocal/BrowserStackLocal/BrowserStackTunnel.cs b/BrowserStackLocal/BrowserStackLocal/BrowserStackTunnel.cs index f825c29..ce109a0 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,48 @@ private string fetchSourceUrl(string accessKey) throw new Exception((string)jsonResponse["error"]); } - sourceUrl = jsonResponse["data"]?["endpoint"]?.ToString(); + sourceUrl = ValidateSourceUrl(jsonResponse["data"]?["endpoint"]?.ToString()); return sourceUrl; } } + // 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)) + { + 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, "..");