docusealco/docuseal · error · DownloadUtils::UnableToDownload

Error loading: #{uri}. Can't download from localhost.

Error message

Error loading: #{uri}. Can't download from localhost.

What it means

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

Source

Thrown at lib/download_utils.rb:57

    uri = begin
      URI(url)
    rescue URI::Error
      Addressable::URI.parse(url).normalize
    end

    validate_uri!(uri) if validate

    resp = conn(validate:).get(uri)

    raise UnableToDownload, "Error loading: #{uri}" if resp.status >= 400

    resp
  end

  def validate_uri!(uri)
    raise UnableToDownload, "Error loading: #{uri}. Only HTTPS is allowed." if uri.scheme != 'https' ||
                                                                               [443, nil].exclude?(uri.port)
    raise UnableToDownload, "Error loading: #{uri}. Can't download from localhost." if uri.host.in?(LOCALHOSTS)
  end

  def conn(validate: Docuseal.multitenant?)
    Faraday.new do |faraday|
      faraday.response :follow_redirects, callback: lambda { |_, new_env|
        validate_uri!(new_env[:url]) if validate
      }
    end
  end
end

View on GitHub (pinned to 004a22c1c8)

Solutions

  1. Host the file on a public HTTPS host and use that URL.
  2. For a legitimate internal integration on self-hosted, run in non-multitenant mode where validation is skipped - and accept the risk consciously.
  3. Expose the internal service through an authenticated public endpoint instead of loopback.
  4. If you must allow a specific internal host, audit the LOCALHOSTS set and understand why the host is listed before touching it.

Example fix

# before
attach_url: 'http://localhost:3000/files/doc.pdf'

# after
attach_url: 'https://public-host.example.com/files/doc.pdf'
Defensive patterns

Strategy: validation

Validate before calling

host = URI(url).host
raise UnableToDownload, 'localhost blocked' if DownloadUtils::LOCALHOSTS.include?(host)

Try / catch

rescue UnableToDownload => e
  # do not retry: the SSRF guard is a terminal rejection by design
  report_invalid_source_url(e.message)

Prevention

When it happens

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

Common situations: Developers testing URL-download features against local fixtures; accidental attempts to pull internal services or cloud metadata; shared dev environments with loopback aliases.

Related errors


AI-assisted analysis of docusealco/docuseal@004a22c1c8 (2026-08-21). Data as JSON: /api/errors/a6ea9800aa29383b. Report an issue: GitHub.