Configurable endpoint for S3 Adaptor#3520
Conversation
0352a92 to
7218618
Compare
Add SSRF protection for the configurable S3 endpoint by rejecting endpoints that resolve to private or reserved IP addresses, plus tests. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
| host = URI.parse(endpoint).host | ||
|
|
||
| case SSRF.safe_resolve(host) do | ||
| {:ok, _} -> [] |
There was a problem hiding this comment.
🟠 Severity: HIGH
The resolved IP from SSRF.safe_resolve/1 is discarded ({:ok, _} -> []). The original hostname is stored and later passed directly to the FSS S3 client, which re-resolves DNS at request time. For HTTP endpoints, this enables DNS rebinding: an attacker registers a domain that resolves to a public IP at validation, then switches the DNS record to a private address (e.g. 169.254.169.254) before requests are made — bypassing the SSRF check entirely.
Helpful? Add 👍 / 👎
💡 Fix Suggestion
Suggestion: The DNS rebinding bypass exists because no_ssrf_validator/2 discards the resolved IP address ({:ok, _} -> []) and the original hostname is stored in the changeset, which fss_s3_config/1 later passes directly to the FSS S3 client — allowing DNS re-resolution at request time.
The fix requires two coordinated changes in the same file:
1. Replace line 72 in validate_config/1, changing:
|> Changeset.validate_change(:endpoint, &no_ssrf_validator/2)to:
|> pin_endpoint_against_ssrf()2. Replace the entire no_ssrf_validator/2 function (lines 75-82) with a new function that both validates AND rewrites the endpoint to pin the resolved IP:
defp pin_endpoint_against_ssrf(%Changeset{} = changeset) do
case Changeset.fetch_change(changeset, :endpoint) do
{:ok, endpoint} ->
host = URI.parse(endpoint).host
case SSRF.safe_resolve(host) do
{:ok, ip} ->
uri = URI.parse(endpoint)
ip_host = SSRF.url_host(ip)
pinned = URI.to_string(%{uri | host: ip_host, authority: nil})
Changeset.put_change(changeset, :endpoint, pinned)
{:error, reason} ->
Changeset.add_error(changeset, :endpoint, reason, validation: :ssrf)
end
:error ->
changeset
end
endBy rewriting the endpoint in the changeset to use the resolved IP address at validation time (mirroring the approach in SSRFProtection middleware for HTTP adaptor), fss_s3_config/1 will receive the IP-pinned endpoint and the FSS S3 client will connect directly to it — eliminating the DNS re-resolution window exploited by DNS rebinding. SSRF.url_host/1 is already available in the module via the SSRF alias and handles IPv6 bracket formatting correctly.
Replace the IP-denylist SSRF check on the S3 endpoint with a hostname
allowlist gated by tenancy. The previous approach resolved the host but
passed the hostname to the FSS.S3 client, which re-resolves DNS itself,
leaving a DNS-rebinding/dual-stack TOCTOU window. An allowlist of known
S3-compatible providers removes the attacker's control over DNS, which is
the precondition for the bypass.
- Multi-tenant: endpoint host must match a trusted provider suffix
- Single-tenant: any endpoint allowed (operator trusts own infra)
- Remove the runtime check_no_ssrf!/1 raise; validation lives at
config-save time and test_connection/1 keeps its :ok | {:error, term()}
contract
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the single-tenant gating of the S3 custom-endpoint allowlist with an explicit escape-hatch env var, LOGFLARE_UNSAFE_DISABLE_SSRF_S3_ENDPOINT_CHECK. The allowlist is enforced by default; only an explicit truthy value disables it, for trusted self-hosted deployments using non-public S3-compatible storage (e.g. internal MinIO). Documented in the self-hosting env-var table with an SSRF warning. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
No description provided.