{"record":{"id":"a6ea9800aa29383b","repo":"docusealco/docuseal","slug":"error-loading-uri-can-t-download-from-localho","errorCode":null,"errorMessage":"Error loading: #{uri}. Can't download from localhost.","messagePattern":"Error loading: #(.+?)\\. Can't download from localhost\\.","errorType":"exception","errorClass":"DownloadUtils::UnableToDownload","httpStatus":422,"severity":"error","filePath":"lib/download_utils.rb","lineNumber":57,"sourceCode":"    uri = begin\n      URI(url)\n    rescue URI::Error\n      Addressable::URI.parse(url).normalize\n    end\n\n    validate_uri!(uri) if validate\n\n    resp = conn(validate:).get(uri)\n\n    raise UnableToDownload, \"Error loading: #{uri}\" if resp.status >= 400\n\n    resp\n  end\n\n  def validate_uri!(uri)\n    raise UnableToDownload, \"Error loading: #{uri}. Only HTTPS is allowed.\" if uri.scheme != 'https' ||\n                                                                               [443, nil].exclude?(uri.port)\n    raise UnableToDownload, \"Error loading: #{uri}. Can't download from localhost.\" if uri.host.in?(LOCALHOSTS)\n  end\n\n  def conn(validate: Docuseal.multitenant?)\n    Faraday.new do |faraday|\n      faraday.response :follow_redirects, callback: lambda { |_, new_env|\n        validate_uri!(new_env[:url]) if validate\n      }\n    end\n  end\nend\n","sourceCodeStart":39,"sourceCodeEnd":68,"githubUrl":"https://github.com/docusealco/docuseal/blob/004a22c1c88109c7ba0b567df011a8cb13894001/lib/download_utils.rb#L39-L68","documentation":"The same validate_uri! rejects downloads whose host is in LOCALHOSTS - an explicit set covering IPv4/IPv6 loopback forms (localhost, 127.0.0.1, ::1, 0.0.0.0, ip6-loopback and variants) - appending '. Can't download from localhost.'. This is SSRF protection: it stops multitenant workloads from fetching files off the server's own loopback interfaces. It also re-checks every redirect hop, so an external URL redirecting to 127.0.0.1 raises too; the same constant backs webhook egress protection (SendWebhookRequest::LocalhostError).","triggerScenarios":"Passing http(s)://localhost/... or 127.0.0.1 URLs as file sources; hostnames resolving to loopback; redirect chains ending on a loopback host; testing integrations against a local dev server from a validating instance.","commonSituations":"Developers testing URL-download features against local fixtures; accidental attempts to pull internal services or cloud metadata; shared dev environments with loopback aliases.","solutions":["Host the file on a public HTTPS host and use that URL.","For a legitimate internal integration on self-hosted, run in non-multitenant mode where validation is skipped - and accept the risk consciously.","Expose the internal service through an authenticated public endpoint instead of loopback.","If you must allow a specific internal host, audit the LOCALHOSTS set and understand why the host is listed before touching it."],"exampleFix":"# before\nattach_url: 'http://localhost:3000/files/doc.pdf'\n\n# after\nattach_url: 'https://public-host.example.com/files/doc.pdf'","handlingStrategy":"validation","validationCode":"host = URI(url).host\nraise UnableToDownload, 'localhost blocked' if DownloadUtils::LOCALHOSTS.include?(host)","typeGuard":null,"tryCatchPattern":"rescue UnableToDownload => e\n  # do not retry: the SSRF guard is a terminal rejection by design\n  report_invalid_source_url(e.message)","preventionTips":["Never accept loopback or internal URLs from user input","Validate redirect targets, not just the initial URL","Resolve untrusted hostnames and check for loopback answers"],"tags":["ssrf","localhost","download","url-validation","security"],"backgroundTag":"ssrf-localhost-blocked","analyzedSha":"004a22c1c88109c7ba0b567df011a8cb13894001","analyzedAt":"2026-08-21T13:38:23.343Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}