{"record":{"id":"43b9b944164df45c","repo":"Freika/dawarich","slug":"url-resolves-to-a-blocked-address","errorCode":null,"errorMessage":"URL resolves to a blocked address","messagePattern":"URL resolves to a blocked address","errorType":"validation","errorClass":"UrlValidatable::BlockedUrlError","httpStatus":null,"severity":"warning","filePath":"app/services/concerns/url_validatable.rb","lineNumber":77,"sourceCode":"  def validate_integration_url!(url)\n    return if url.blank?\n\n    uri = URI.parse(url)\n    unless %w[http https].include?(uri.scheme)\n      raise BlockedUrlError, I18n.t('services.concerns.url_validatable.invalid_scheme', scheme: uri.scheme)\n    end\n    raise BlockedUrlError, I18n.t('services.concerns.url_validatable.host_required') if uri.host.blank?\n\n    # Cloud refuses URLs that embed credentials. Self-hosters legitimately\n    # use http://user:pass@host — homelab Immich behind nginx basic-auth\n    # is a real config we don't want to break.\n    if uri.userinfo.present? && !DawarichSettings.self_hosted?\n      raise BlockedUrlError, I18n.t('services.concerns.url_validatable.embedded_credentials')\n    end\n\n    ip = IPAddr.new(Resolv.getaddress(uri.host))\n    if blocked_ranges.any? { |range| range.include?(ip) }\n      raise BlockedUrlError, I18n.t('services.concerns.url_validatable.blocked_address')\n    end\n  rescue URI::InvalidURIError\n    raise BlockedUrlError, I18n.t('services.concerns.url_validatable.invalid_format')\n  rescue Resolv::ResolvError\n    raise BlockedUrlError, I18n.t('services.concerns.url_validatable.unresolvable_host', host: uri.host)\n  end\n\n  def blocked_ranges\n    if DawarichSettings.self_hosted?\n      ALWAYS_BLOCKED_RANGES\n    else\n      ALWAYS_BLOCKED_RANGES + CLOUD_ONLY_BLOCKED_RANGES\n    end\n  end\nend\n","sourceCodeStart":59,"sourceCodeEnd":93,"githubUrl":"https://github.com/Freika/dawarich/blob/97fad417c5a11b0eb11157890635e015723a2e97/app/services/concerns/url_validatable.rb#L59-L93","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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.","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).","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.","Check what the host actually resolves to from the server: Resolv.getaddress(host) in a Rails console."],"exampleFix":"# before (cloud)\nurl = 'http://192.168.1.50:2283' # resolves to private range -> BlockedUrlError\n\n# after\nurl = 'https://immich.yourdomain.com' # public IP via tunnel/port-forward","handlingStrategy":"validation","validationCode":"ip = IPAddr.new(Resolv.getaddress(URI.parse(url).host)) rescue nil\nblocked = [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')]\nblocked.any? { |r| r.include?(ip) } # pre-flight SSRF check","typeGuard":"def public_http_url?(s)\n  u = URI.parse(s.to_s)\n  return false unless %w[http https].include?(u.scheme) && u.host\n  ip = IPAddr.new(Resolv.getaddress(u.host))\n  !ip.private? && !ip.loopback?\nrescue StandardError\n  false\nend","tryCatchPattern":"begin\n  validate_integration_url!(url)\nrescue BlockedUrlError => e\n  render json: { error: 'Endpoint must be publicly reachable' }, status: :unprocessable_entity\nend","preventionTips":["On cloud, expose homelab services through a tunnel before linking them.","On self-host, confirm the self_hosted setting is correct so the intended (narrower) rule set applies.","Document for users that localhost/LAN URLs cannot work from a hosted server."],"tags":["url-validation","ssrf","security","network","integrations","ruby"],"backgroundTag":"ssrf-blocked-address","analyzedSha":"97fad417c5a11b0eb11157890635e015723a2e97","analyzedAt":"2026-08-21T17:04:17.778Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}