{"record":{"id":"92e12af6466cb164","repo":"lostindark/DriverStoreExplorer","slug":"download-url-is-not-from-a-trusted-github-domain","errorCode":null,"errorMessage":"Download URL is not from a trusted GitHub domain.","messagePattern":"Download URL is not from a trusted GitHub domain\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"Rapr/UpdateManager.cs","lineNumber":80,"sourceCode":"                        DownloadUrl = new Uri(downloadUrl),\n                        Sha256 = sha256\n                    };\n                }\n\n                return null;\n            }\n        }\n\n        public async Task ApplyUpdateAsync(VersionInfo versionInfo, IProgress<float> progress)\n        {\n            if (versionInfo == null)\n            {\n                throw new ArgumentNullException(nameof(versionInfo));\n            }\n\n            if (!IsGitHubUrl(versionInfo.DownloadUrl))\n            {\n                throw new InvalidOperationException(\"Download URL is not from a trusted GitHub domain.\");\n            }\n\n            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;\n\n            string tempBaseDir = Path.Combine(Path.GetTempPath(), \"DriverStoreExplorer\");\n            string downloadFileName = Path.GetFileName(versionInfo.DownloadUrl.LocalPath);\n            string tempZipPath = Path.Combine(tempBaseDir, downloadFileName);\n            string tempExtractPath = Path.Combine(tempBaseDir, \"Update\");\n\n            if (!Directory.Exists(tempBaseDir))\n            {\n                Directory.CreateDirectory(tempBaseDir);\n            }\n\n            // Clean up any previous update artifacts\n            if (File.Exists(tempZipPath))\n            {\n                File.Delete(tempZipPath);","sourceCodeStart":62,"sourceCodeEnd":98,"githubUrl":"https://github.com/lostindark/DriverStoreExplorer/blob/958fcd481bd3e212c3dcf1e531b3e332d5a8e81a/Rapr/UpdateManager.cs#L62-L98","documentation":"A security pre-check inside UpdateManager.ApplyUpdateAsync. Before any network I/O, IsGitHubUrl verifies the DownloadUrl scheme is HTTPS and the host is github.com, *.github.com, or *.githubusercontent.com. This blocks SSRF, supply-chain swaps, and accidental fetches from attacker-controlled mirrors. The check fails closed: any release whose browser_download_url leaves the GitHub domain set aborts the whole update.","triggerScenarios":"versionInfo.DownloadUrl is built in GetLatestVersionInfo from releaseInfo[\"assets[0].browser_download_url\"]. If a release attaches an asset whose URL is HTTPS but on a CDN outside *.githubusercontent.com, or a caller hand-constructs a VersionInfo pointing elsewhere, IsGitHubUrl (line 199) returns false and line 80 throws.","commonSituations":"A fork re-hosts the release zip on a private server; a GitHub Enterprise release points to an enterprise host (not *.github.com); a test VersionInfo built in a unit test uses localhost or example.com; a proxy rewrites the asset URL; the GitHub API response is mocked with a non-GitHub asset URL.","solutions":["Keep release assets on GitHub so browser_download_url stays on objects.githubusercontent.com — the supported, default path.","If you must allow an additional trusted host, extend IsGitHubUrl's host whitelist rather than disabling the check.","Inspect the release JSON: curl -sSL https://api.github.com/repos/<owner>/<repo>/releases/latest | jq '.assets[].browser_download_url'.","For GitHub Enterprise, add the enterprise host (e.g. github.<company>.com) to the host check.","In tests, build VersionInfo with a Uri(\"https://github.com/owner/repo/releases/download/v1/x.zip\")."],"exampleFix":"// before\nprivate static bool IsGitHubUrl(Uri url)\n{\n    return url.Scheme == Uri.UriSchemeHttps\n        && (url.Host.Equals(\"github.com\", StringComparison.OrdinalIgnoreCase)\n            || url.Host.EndsWith(\".github.com\", StringComparison.OrdinalIgnoreCase)\n            || url.Host.EndsWith(\".githubusercontent.com\", StringComparison.OrdinalIgnoreCase));\n}\n\n// after — allow a configurable allowlist for GitHub Enterprise / mirrors\nprivate static readonly string[] TrustedHostSuffixes =\n{\n    \"github.com\",\n    \"githubusercontent.com\",\n    \"github.mycompany.com\",\n};\n\nprivate static bool IsTrustedUrl(Uri url)\n{\n    if (url == null || url.Scheme != Uri.UriSchemeHttps) return false;\n    foreach (var suffix in TrustedHostSuffixes)\n    {\n        if (url.Host.Equals(suffix, StringComparison.OrdinalIgnoreCase)\n            || url.Host.EndsWith(\".\" + suffix, StringComparison.OrdinalIgnoreCase))\n            return true;\n    }\n    return false;\n}","handlingStrategy":"validation","validationCode":"// Validate before invoking ApplyUpdateAsync\nprivate static bool TryGetSafeDownloadUrl(VersionInfo info, out Uri url, out string error)\n{\n    url = null;\n    error = null;\n    if (info?.DownloadUrl == null) { error = \"No download URL present.\"; return false; }\n    if (info.DownloadUrl.Scheme != Uri.UriSchemeHttps) { error = \"Download URL must use HTTPS.\"; return false; }\n\n    string host = info.DownloadUrl.Host;\n    bool trusted = host.Equals(\"github.com\", StringComparison.OrdinalIgnoreCase)\n        || host.EndsWith(\".github.com\", StringComparison.OrdinalIgnoreCase)\n        || host.EndsWith(\".githubusercontent.com\", StringComparison.OrdinalIgnoreCase);\n    if (!trusted) { error = \"Download URL host is not a trusted GitHub domain: \" + host; return false; }\n\n    url = info.DownloadUrl;\n    return true;\n}\n\n// Usage:\nif (!TryGetSafeDownloadUrl(this.latestVersionInfo, out var url, out var err))\n{\n    MessageBox.Show(err, Language.Product_Name, MessageBoxButtons.OK, MessageBoxIcon.Warning);\n    return;\n}","typeGuard":"private static bool IsTrustedGitHubReleaseUrl(VersionInfo info)\n    => info?.DownloadUrl is Uri u\n       && u.Scheme == Uri.UriSchemeHttps\n       && (u.Host.Equals(\"github.com\", StringComparison.OrdinalIgnoreCase)\n           || u.Host.EndsWith(\".github.com\", StringComparison.OrdinalIgnoreCase)\n           || u.Host.EndsWith(\".githubusercontent.com\", StringComparison.OrdinalIgnoreCase));","tryCatchPattern":null,"preventionTips":["Keep all release assets on GitHub so browser_download_url always resolves under objects.githubusercontent.com.","When forking, either re-host on GitHub or extend IsGitHubUrl's host list explicitly.","For GitHub Enterprise, add the enterprise host suffix to the whitelist.","In tests, build VersionInfo with a Uri on https://github.com/ to avoid tripping the guard.","Never construct a VersionInfo from untrusted user/URL input without routing it through the same host check first."],"tags":["security","url-validation","ssrf","update","https","domain-allowlist"],"backgroundTag":null,"analyzedSha":"958fcd481bd3e212c3dcf1e531b3e332d5a8e81a","analyzedAt":"2026-08-13T19:08:09.376Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}