chatwoot/lib/safe_fetch.rb
Vishnu Narayanan 7c16071fc7
fix: Support allowlisted private API inbox webhooks (#14548)
Self-hosted installations can now opt SafeFetch into private-network
access after SSRF hardening. The default remains unchanged: private IP
destinations are blocked unless the instance owner explicitly enables
private-network requests with `SAFE_FETCH_ALLOW_PRIVATE_NETWORK=true`.

Fixes https://linear.app/chatwoot/issue/CW-7131
Fixes https://github.com/chatwoot/chatwoot/issues/14489
Fixes https://github.com/chatwoot/chatwoot/issues/14494

## How to use

For self-hosted installations that need API inbox webhooks, or other
SafeFetch-backed requests, to call trusted private services, enable
private-network access with a single environment variable:

```bash
SAFE_FETCH_ALLOW_PRIVATE_NETWORK=true
```

This is disabled by default. Enable it only when the instance owner
controls the deployment network and trusts the configured URLs.
2026-05-26 17:03:19 +05:30

42 lines
1.3 KiB
Ruby

require 'ssrf_filter'
module SafeFetch
DEFAULT_ALLOWED_CONTENT_TYPE_PREFIXES = %w[image/ video/].freeze
DEFAULT_ALLOWED_CONTENT_TYPES = [].freeze
DEFAULT_SENSITIVE_HEADERS = %w[authorization cookie proxy-authorization].freeze
DEFAULT_OPEN_TIMEOUT = 2
DEFAULT_READ_TIMEOUT = 20
DEFAULT_MAX_BYTES_FALLBACK_MB = 40
Result = Data.define(:tempfile, :filename, :content_type) do
def original_filename
filename
end
end
class Error < StandardError; end
class InvalidUrlError < Error; end
class UnsafeUrlError < Error; end
class FetchError < Error; end
class HttpError < Error; end
class FileTooLargeError < Error; end
class UnsupportedContentTypeError < Error; end
class UnsupportedMethodError < Error; end
def self.fetch(url, **, &)
raise ArgumentError, 'block required' unless block_given?
SafeFetch::Fetcher.new(SafeFetch::RequestOptions.new(url: url, **)).fetch(&)
rescue SsrfFilter::InvalidUriScheme, URI::InvalidURIError => e
raise InvalidUrlError, e.message
rescue SsrfFilter::Error, Resolv::ResolvError => e
raise UnsafeUrlError, e.message
rescue Net::OpenTimeout, Net::ReadTimeout, SocketError, OpenSSL::SSL::SSLError => e
raise FetchError, e.message
end
def self.allow_private_network?
ActiveModel::Type::Boolean.new.cast(ENV.fetch('SAFE_FETCH_ALLOW_PRIVATE_NETWORK', false))
end
end