{"record":{"id":"dc7b90c4fa4f44f7","repo":"Kareadita/Kavita","slug":"url-blocked-address-dc7b90","errorCode":null,"errorMessage":"url-blocked-address","messagePattern":"url-blocked-address","errorType":"exception","errorClass":"KavitaException","httpStatus":400,"severity":"error","filePath":"Kavita.Services/UrlValidationService.cs","lineNumber":44,"sourceCode":"        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)\n        {\n            if (IpBlocklist.IsBlockedAddress(address))\n            {\n                throw new KavitaException(await localizationService.TranslateAsync(\"url-blocked-address\"));\n            }\n        }\n    }\n}\n","sourceCodeStart":26,"sourceCodeEnd":49,"githubUrl":"https://github.com/Kareadita/Kavita/blob/9c3e5400007f8a0282f7d883f2ad5e71716e514d/Kavita.Services/UrlValidationService.cs#L26-L49","documentation":"Thrown by UrlValidationService.ValidateUrlAsync when any resolved IP for the host is on IpBlocklist.IsBlockedAddress — loopback, private (10/8, 172.16/12, 192.168/16), CGNAT, link-local (169.254/16, AWS metadata), multicast, reserved, documentation/test-net, and IPv6 equivalents. This is Kavita's core SSRF defense: a user-supplied URL may not pull content from internal/private addresses. It is a localized KavitaException surfaced as HTTP 500 (or 400 where the caller wraps it).","triggerScenarios":"ValidateUrlAsync receives an https URL whose host resolves (or literally is) an internal/private IP: localhost / 127.0.0.1, 192.168.x.x, 10.x.x.x, 172.16-31.x.x, 169.254.169.254 (cloud metadata), or a hostname that DNS-rebinds to such an address. Reachable from cover/favicon/font/CBL/upload callers.","commonSituations":"User tries to pull a cover or CBL from an internal/LAN host; a DNS-rebinding payload points a public-looking name at 127.0.0.1; referencing the Kavita server's own internal address; attempting to reach the 169.254.169.254 metadata endpoint.","solutions":["Use a URL whose host resolves to a public IP; host the asset on a publicly addressable https endpoint.","If the content lives on the LAN, proxy it through Kavita or a public reverse proxy rather than passing an internal URL.","Do not attempt to disable the blocklist; it is a security control — instead expose the resource over a public address."],"exampleFix":"// before\nconst url = 'https://192.168.1.5/cover.png'; // blocked\n// after\nconst url = 'https://cdn.example.com/cover.png'; // public IP only","handlingStrategy":"validation","validationCode":"function isPublicHost(url: string): boolean {\n  try {\n    const h = new URL(url).hostname;\n    // reject literal private/loopback IPs and link-local metadata hostnames\n    if (/^(127\\.|10\\.|192\\.168\\.|169\\.254\\.|172\\.(1[6-9]|2[0-9]|3[01])\\.)/.test(h)) return false;\n    if (h === 'localhost' || h === '0.0.0.0') return false;\n    return true;\n  } catch { return false; }\n}","typeGuard":"function isPublicUrlCandidate(s: unknown): s is string {\n  return typeof s === 'string' && isPublicHost(s);\n}","tryCatchPattern":"try { await svc.fetchFromUrl(url); } catch (e) { if (/blocked address/i.test(e.message)) showUser('That URL points to a private/blocked address; use a public https URL'); else throw e; }","preventionTips":["Only submit URLs whose host resolves to a public IP.","Host internal assets behind a public https reverse proxy instead of passing LAN URLs.","Never attempt to bypass IpBlocklist; it is a security control."],"tags":["url","ssrf","security","ip-blocklist"],"backgroundTag":null,"analyzedSha":"9c3e5400007f8a0282f7d883f2ad5e71716e514d","analyzedAt":"2026-08-13T19:06:05.897Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}