Freika/dawarich · warning · UrlValidatable::BlockedUrlError

URL resolves to a blocked address

Error message

URL resolves to a blocked address

What it means

Raised as BlockedUrlError when the URL's host resolves (via Resolv.getaddress) to an IP inside a blocked range — SSRF protection. ALWAYS_BLOCKED_RANGES covers loopback, private, link-local, and similar (127.0.0.0/8, 10/8, 172.16/12, 192.168/16, 169.254/16, ::1, fc00::/7, plus benchmark 198.18/15); non-self-hosted instances additionally apply CLOUD_ONLY_BLOCKED_RANGES. The error means the server refused to be pointed at its own or its network's internal addresses.

Source

Thrown at app/services/concerns/url_validatable.rb:77

  def validate_integration_url!(url)
    return if url.blank?

    uri = URI.parse(url)
    unless %w[http https].include?(uri.scheme)
      raise BlockedUrlError, I18n.t('services.concerns.url_validatable.invalid_scheme', scheme: uri.scheme)
    end
    raise BlockedUrlError, I18n.t('services.concerns.url_validatable.host_required') if uri.host.blank?

    # Cloud refuses URLs that embed credentials. Self-hosters legitimately
    # use http://user:pass@host — homelab Immich behind nginx basic-auth
    # is a real config we don't want to break.
    if uri.userinfo.present? && !DawarichSettings.self_hosted?
      raise BlockedUrlError, I18n.t('services.concerns.url_validatable.embedded_credentials')
    end

    ip = IPAddr.new(Resolv.getaddress(uri.host))
    if blocked_ranges.any? { |range| range.include?(ip) }
      raise BlockedUrlError, I18n.t('services.concerns.url_validatable.blocked_address')
    end
  rescue URI::InvalidURIError
    raise BlockedUrlError, I18n.t('services.concerns.url_validatable.invalid_format')
  rescue Resolv::ResolvError
    raise BlockedUrlError, I18n.t('services.concerns.url_validatable.unresolvable_host', host: uri.host)
  end

  def blocked_ranges
    if DawarichSettings.self_hosted?
      ALWAYS_BLOCKED_RANGES
    else
      ALWAYS_BLOCKED_RANGES + CLOUD_ONLY_BLOCKED_RANGES
    end
  end
end

View on GitHub (pinned to 97fad417c5)

Solutions

  1. Use a URL whose host resolves to a publicly reachable IP (port-forward/Tunnel the service, e.g. via Tailscale Funnel/Cloudflare Tunnel) when on cloud.
  2. On your own deployment, verify DawarichSettings.self_hosted? returns true — that narrows blocking to ALWAYS_BLOCKED_RANGES only (note loopback/private stay blocked even then).
  3. If you truly need localhost integrations self-hosted, run the integration on a non-loopback, non-RFC1918 address or adjust the concern deliberately, accepting the SSRF exposure.
  4. Check what the host actually resolves to from the server: Resolv.getaddress(host) in a Rails console.

Example fix

# before (cloud)
url = 'http://192.168.1.50:2283' # resolves to private range -> BlockedUrlError

# after
url = 'https://immich.yourdomain.com' # public IP via tunnel/port-forward
Defensive patterns

Strategy: validation

Validate before calling

ip = IPAddr.new(Resolv.getaddress(URI.parse(url).host)) rescue nil
blocked = [IPAddr.new('127.0.0.0/8'), IPAddr.new('10.0.0.0/8'), IPAddr.new('172.16.0.0/12'), IPAddr.new('192.168.0.0/16'), IPAddr.new('169.254.0.0/16'), IPAddr.new('::1/128'), IPAddr.new('fc00::/7')]
blocked.any? { |r| r.include?(ip) } # pre-flight SSRF check

Type guard

def public_http_url?(s)
  u = URI.parse(s.to_s)
  return false unless %w[http https].include?(u.scheme) && u.host
  ip = IPAddr.new(Resolv.getaddress(u.host))
  !ip.private? && !ip.loopback?
rescue StandardError
  false
end

Try / catch

begin
  validate_integration_url!(url)
rescue BlockedUrlError => e
  render json: { error: 'Endpoint must be publicly reachable' }, status: :unprocessable_entity
end

Prevention

When it happens

Trigger: Entering 'http://localhost:2283' or 'http://192.168.1.50:2283' as an integration URL on the hosted cloud (host resolves to loopback/private space and cloud ranges are in force); DNS names that resolve to internal IPs, such as a homelab dyn-domain from inside the cluster; a hostname whose public DNS has an A record in 10.0.0.0/8.

Common situations: Cloud users trying to point the service at machines on their home network (impossible by design — the cloud server cannot reach them anyway), self-hosters on localhost who have self_hosted? misconfigured false, split-horizon DNS where the name is public but resolves privately.

Related errors


AI-assisted analysis of Freika/dawarich@97fad417c5 (2026-08-21). Data as JSON: /api/errors/43b9b944164df45c. Report an issue: GitHub.