Kareadita/Kavita · error · KavitaException

url-blocked-address

Error message

url-blocked-address

What it means

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).

Source

Thrown at Kavita.Services/UrlValidationService.cs:44

        try
        {
            addresses = await Dns.GetHostAddressesAsync(uri.Host);
        }
        catch (SocketException)
        {
            throw new KavitaException(await localizationService.TranslateAsync("url-unable-to-resolve"));
        }

        if (addresses.Length == 0)
        {
            throw new KavitaException(await localizationService.TranslateAsync("url-unable-to-resolve"));
        }

        foreach (var address in addresses)
        {
            if (IpBlocklist.IsBlockedAddress(address))
            {
                throw new KavitaException(await localizationService.TranslateAsync("url-blocked-address"));
            }
        }
    }
}

View on GitHub (pinned to 9c3e540000)

Solutions

  1. Use a URL whose host resolves to a public IP; host the asset on a publicly addressable https endpoint.
  2. If the content lives on the LAN, proxy it through Kavita or a public reverse proxy rather than passing an internal URL.
  3. Do not attempt to disable the blocklist; it is a security control — instead expose the resource over a public address.

Example fix

// before
const url = 'https://192.168.1.5/cover.png'; // blocked
// after
const url = 'https://cdn.example.com/cover.png'; // public IP only
Defensive patterns

Strategy: validation

Validate before calling

function isPublicHost(url: string): boolean {
  try {
    const h = new URL(url).hostname;
    // reject literal private/loopback IPs and link-local metadata hostnames
    if (/^(127\.|10\.|192\.168\.|169\.254\.|172\.(1[6-9]|2[0-9]|3[01])\.)/.test(h)) return false;
    if (h === 'localhost' || h === '0.0.0.0') return false;
    return true;
  } catch { return false; }
}

Type guard

function isPublicUrlCandidate(s: unknown): s is string {
  return typeof s === 'string' && isPublicHost(s);
}

Try / catch

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; }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of Kareadita/Kavita@9c3e540000 (2026-08-13). Data as JSON: /api/errors/dc7b90c4fa4f44f7. Report an issue: GitHub.