{"record":{"id":"3d8b542644ba608c","repo":"instructure/canvas-lms","slug":"canvas-http-warning-host-host-invalid-ip-ip","errorCode":null,"errorMessage":"CANVAS_HTTP WARNING | host: #{host} | invalid_ip: #{ip}","messagePattern":"CANVAS_HTTP WARNING \\| host: #(.+?) \\| invalid_ip: #(.+?)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"gems/canvas_http/lib/canvas_http.rb","lineNumber":283,"sourceCode":"    [value, uri]\n  end\n\n  def self.insecure_host?(host)\n    return false if blocked_ip_ranges.empty?\n\n    resolved_addrs = Resolv.getaddresses(host)\n    unless resolved_addrs.any?\n      # this is actually a different condition than the host being insecure,\n      # and having separate telemetry is helpful for understanding transient failures.\n      raise UnresolvableUriError, \"#{host} cannot be resolved to any address\"\n    end\n\n    ip_addrs = resolved_addrs.filter_map do |ip|\n      ::IPAddr.new(ip)\n    rescue IPAddr::InvalidAddressError\n      # this should never happen, Resolv should only be passing back IPs, but\n      # let's make sure we can see if the impossible occurs\n      logger.warn(\"CANVAS_HTTP WARNING | host: #{host} | invalid_ip: #{ip}\")\n      nil\n    end\n    unless ip_addrs.any?\n      raise UnresolvableUriError, \"#{host} resolves to only unparseable IPs...\"\n    end\n\n    blocked_ip_ranges.each do |range|\n      addr_range = ::IPAddr.new(range)\n      ip_addrs.any? do |addr|\n        if addr_range.include?(addr)\n          logger.warn(\"CANVAS_HTTP WARNING insecure address | host: #{host} | insecure_address: #{addr} | range: #{range}\")\n          return true\n        end\n      end\n    end\n    false\n  end\n","sourceCodeStart":265,"sourceCodeEnd":301,"githubUrl":"https://github.com/instructure/canvas-lms/blob/1c9f0bb8013ed69c4f2efe11fd483025469b7e6c/gems/canvas_http/lib/canvas_http.rb#L265-L301","documentation":"CanvasHttp's SSRF protection resolves the host and parses each resolved address into IPAddr objects. If Resolv ever returns a value that isn't a parseable IP, this warning is logged per-address and the entry is dropped. If no addresses remain parseable, an UnresolvableUriError is raised. The warning itself means the 'impossible' case occurred.","triggerScenarios":"resolved_addrs contains a string that IPAddr.new cannot parse (IPAddr::InvalidAddressError) while validating host before an outbound fetch — typically due to odd resolver output, IPv6 zone IDs, or custom DNS returning non-IP strings.","commonSituations":"Hostnames resolving through unusual resolver configurations; IPv6 literals with scope/zone identifiers (fe80::1%eth0); mocking/stubbing Resolv in tests with fake address strings; hosts-file entries with malformed values.","solutions":["Inspect what Resolv returns for the host (`Resolv.getaddresses host`) and fix the resolver/DNS config","Strip IPv6 zone identifiers before validation if hosts contain %scope suffixes","Update the parsing to normalize addresses (e.g., strip brackets from bracketed IPv6 literals) if applicable","If only the warning fires but some IPs parse, behavior is safe — but investigate why Resolv emitted a non-IP"],"exampleFix":"// before\nip = 'fe80::1%eth0'\nIPAddr.new(ip) # InvalidAddressError -> warning\n// after\nip = ip.split('%').first\nIPAddr.new(ip) # parses link-local address\n","handlingStrategy":"validation","validationCode":"def parseable_ip?(addr)\n  IPAddr.new(addr.to_s.sub(/%.*$/, ''))\n  true\nrescue IPAddr::InvalidAddressError\n  false\nend","typeGuard":"def valid_ip?(str)\n  IPAddr.new(str.to_s)\n  true\nrescue IPAddr::InvalidAddressError\n  false\nend","tryCatchPattern":"begin\n  CanvasHttp.get(url)\nrescue CanvasHttp::UnresolvableUriError => e\n  logger.error(\"host unresolvable/invalid: #{e.message}\")\nend","preventionTips":["Strip IPv6 zone identifiers and brackets before DNS-related validation","Test CanvasHttp with exotic hostnames (IPv6 literals, hosts-file entries)","Keep Resolv output expectations documented when stubbing DNS in tests"],"tags":["ssrf","dns","ip-address","canvas-http"],"backgroundTag":"invalid-url-format","analyzedSha":"1c9f0bb8013ed69c4f2efe11fd483025469b7e6c","analyzedAt":"2026-09-15T20:33:18.891Z","contentChangedAt":"2026-09-15T20:33:18.891Z","schemaVersion":2},"datasetVersion":"2026-09-23T02:17:17.105Z"}