{"record":{"id":"fdedf1d9232338fd","repo":"Kareadita/Kavita","slug":"url-malformed","errorCode":null,"errorMessage":"url-malformed","messagePattern":"url-malformed","errorType":"exception","errorClass":"KavitaException","httpStatus":400,"severity":"warning","filePath":"Kavita.Services/UrlValidationService.cs","lineNumber":17,"sourceCode":"using System;\nusing System.Net;\nusing System.Net.Sockets;\nusing 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)","sourceCodeStart":1,"sourceCodeEnd":35,"githubUrl":"https://github.com/Kareadita/Kavita/blob/9c3e5400007f8a0282f7d883f2ad5e71716e514d/Kavita.Services/UrlValidationService.cs#L1-L35","documentation":"Thrown by UrlValidationService.ValidateUrlAsync when Uri.TryCreate(url, UriKind.Absolute, ...) fails — the input is not a parseable absolute URI. ValidateUrlAsync is Kavita's SSRF pre-flight gate invoked before fetching any user-supplied URL (cover images via CoverDbService/UploadController, favicons, Google Fonts, CBL upload and CBL sync). It is a localized KavitaException surfaced as HTTP 500; some callers (UploadController, CBLController) catch it and return 400 instead.","triggerScenarios":"Any code path that calls ValidateUrlAsync with a value that is not an absolute URI: relative paths, strings with unencoded spaces, missing scheme (e.g. 'example.com/foo'), or garbage. Reachable via upload-by-url, CBL upload-cbl-file, cover-from-url, favicon fetch, font download, and CBL URL sync.","commonSituations":"User pastes a bare domain without https://; a cover URL is copied with a trailing space or newline; a relative '/covers/x.png' is stored where an absolute URL was expected; clipboard copy loses the scheme.","solutions":["Trim and encode the URL, then ensure it starts with https:// before submitting.","Validate client-side with `new URL(url)` (JS) / `Uri.IsWellFormedUriString(url, Absolute)` before calling the API.","If only a host is available, prefix 'https://' on the client so the value is absolute."],"exampleFix":"// before\nuploadByUrl(url: string) { return this.http.post('upload/upload-by-url', { url }); }\n// after\nuploadByUrl(raw: string) {\n  const url = raw.trim();\n  try { if (new URL(url).protocol !== 'https:') throw 0; }\n  catch { return throwError(() => new Error('A valid https:// URL is required')); }\n  return this.http.post('upload/upload-by-url', { url });\n}","handlingStrategy":"validation","validationCode":"function isAbsoluteUrl(url: string): boolean {\n  try { const u = new URL(url.trim()); return u.protocol === 'http:' || u.protocol === 'https:'; }\n  catch { return false; }\n}","typeGuard":"function isAbsoluteHttpsCandidate(s: unknown): s is string {\n  return typeof s === 'string' && isAbsoluteUrl(s);\n}","tryCatchPattern":"try { await svc.fetchFromUrl(url); } catch (e) { if (/malformed/i.test(e.message)) showUser('Enter a valid https:// URL'); else throw e; }","preventionTips":["Trim whitespace/newlines from pasted URLs before submitting.","Always prefix a scheme; bare hosts are rejected.","Validate with `new URL(url)` on the client before the API call."],"tags":["url","ssrf","validation","user-input"],"backgroundTag":null,"analyzedSha":"9c3e5400007f8a0282f7d883f2ad5e71716e514d","analyzedAt":"2026-08-13T19:06:05.897Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}