Skip to content

Configurable endpoint for S3 Adaptor#3520

Open
bblaszkow06 wants to merge 5 commits into
Logflare:mainfrom
bblaszkow06:bb/o11y-1878-s3-endpoint
Open

Configurable endpoint for S3 Adaptor#3520
bblaszkow06 wants to merge 5 commits into
Logflare:mainfrom
bblaszkow06:bb/o11y-1878-s3-endpoint

Conversation

@bblaszkow06

Copy link
Copy Markdown
Contributor

No description provided.

@bblaszkow06 bblaszkow06 force-pushed the bb/o11y-1878-s3-endpoint branch from 0352a92 to 7218618 Compare May 27, 2026 14:28
@bblaszkow06 bblaszkow06 marked this pull request as ready for review May 27, 2026 14:31
Comment thread lib/logflare/backends/adaptor/s3_adaptor.ex
bblaszkow06 and others added 2 commits June 2, 2026 13:38
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, _} -> []

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 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
end

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

bblaszkow06 and others added 2 commits June 10, 2026 17:29
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant