{"record":{"id":"9bd9087bc382bf1c","repo":"Kareadita/Kavita","slug":"url-https-only","errorCode":null,"errorMessage":"url-https-only","messagePattern":"url-https-only","errorType":"exception","errorClass":"KavitaException","httpStatus":400,"severity":"warning","filePath":"Kavita.Services/UrlValidationService.cs","lineNumber":22,"sourceCode":"using System.Threading.Tasks;\nusing Kavita.API.Services;\nusing Kavita.Common;\nusing Kavita.Common.Helpers;\n\nnamespace Kavita.Services;\n\npublic class UrlValidationService(ILocalizationService localizationService) : IUrlValidationService\n{\n    public async Task ValidateUrlAsync(string url)\n    {\n        if (!Uri.TryCreate(url, UriKind.Absolute, out var uri))\n        {\n            throw new KavitaException(await localizationService.TranslateAsync(\"url-malformed\"));\n        }\n\n        if (!string.Equals(uri.Scheme, \"https\", StringComparison.OrdinalIgnoreCase))\n        {\n            throw new KavitaException(await localizationService.TranslateAsync(\"url-https-only\"));\n        }\n\n        IPAddress[] addresses;\n        try\n        {\n            addresses = await Dns.GetHostAddressesAsync(uri.Host);\n        }\n        catch (SocketException)\n        {\n            throw new KavitaException(await localizationService.TranslateAsync(\"url-unable-to-resolve\"));\n        }\n\n        if (addresses.Length == 0)\n        {\n            throw new KavitaException(await localizationService.TranslateAsync(\"url-unable-to-resolve\"));\n        }\n\n        foreach (var address in addresses)","sourceCodeStart":4,"sourceCodeEnd":40,"githubUrl":"https://github.com/Kareadita/Kavita/blob/9c3e5400007f8a0282f7d883f2ad5e71716e514d/Kavita.Services/UrlValidationService.cs#L4-L40","documentation":"Thrown by UrlValidationService.ValidateUrlAsync when the parsed URI's scheme is not https (case-insensitive). Kavita forbids plain http:// (and any non-https scheme) for user-supplied URLs as part of its SSRF/transport-security posture. It is a localized KavitaException surfaced as HTTP 500 (or 400 where callers catch it).","triggerScenarios":"ValidateUrlAsync receives an absolute URI whose scheme is http, ftp, file, etc. Reachable from all the same callers as error 195 (cover/favicon/font/CBL/upload-by-url).","commonSituations":"User pastes an http:// image host; a self-hosted resource is only reachable over http; a config field defaulted to http; an internal service without TLS is referenced.","solutions":["Upgrade the URL to https:// on the client before submitting when the target supports TLS.","Reject non-https input in the form (`new URL(url).protocol === 'https:'`).","If the target truly has no TLS, host the asset on an https endpoint (e.g., put it behind Kavita's own TLS or a reverse proxy)."],"exampleFix":"// before\nconst url = input; // user may type http://\n// after\nlet url = input.trim();\nif (url.startsWith('http://')) url = 'https://' + url.slice('http://'.length);","handlingStrategy":"validation","validationCode":"function isHttpsUrl(url: string): boolean {\n  try { return new URL(url.trim()).protocol === 'https:'; } catch { return false; }\n}","typeGuard":"function isHttpsString(s: unknown): s is string {\n  return typeof s === 'string' && isHttpsUrl(s);\n}","tryCatchPattern":"try { await svc.fetchFromUrl(url); } catch (e) { if (/https/i.test(e.message)) showUser('Only https:// URLs are allowed'); else throw e; }","preventionTips":["Default new URL inputs to https://.","Upgrade http:// to https:// on the client when the target supports TLS.","Reject non-https schemes in form validation."],"tags":["url","ssrf","https","validation"],"backgroundTag":null,"analyzedSha":"9c3e5400007f8a0282f7d883f2ad5e71716e514d","analyzedAt":"2026-08-13T19:06:05.897Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}