Skip to content

SSRF Protection

safeFetch() is a drop-in replacement for fetch() that validates all outbound HTTP requests against private IP ranges and DNS rebinding attacks.

safe-fetch.ts also exports assertUrlAllowed(url) — it runs the same scheme + host/DNS allowlist check without fetching, for clients that manage their own connection (e.g. the MCP SDK transports). It is a no-op when SSRF_PROTECTION_ENABLED=false and throws SSRFBlockedError otherwise.

How It Works

Key behaviors:

  • Manual redirect following — each redirect hop is validated (up to 5 hops)
  • DNS resolution before connect — prevents DNS rebinding where first lookup returns public IP, second returns private
  • DNS cache — 5-minute TTL, prevents repeated lookups for the same host

Blocked IP Ranges

RangeDescription
127.0.0.0/8Loopback
10.0.0.0/8Private (RFC1918)
172.16.0.0/12Private (RFC1918)
192.168.0.0/16Private (RFC1918)
169.254.0.0/16Link-local (cloud metadata)
0.0.0.0/8"This" network
::1IPv6 loopback
fe80::/10IPv6 link-local
fc00::/7IPv6 unique local
::ffff:x.x.x.xIPv4-mapped IPv6 (checks underlying IPv4)

Allowlist

Set SSRF_ALLOWED_HOSTS to a comma-separated list of trusted hosts:

env
SSRF_ALLOWED_HOSTS=oauth.provider.com,*.trusted-domain.com

Wildcard support: *.example.com matches sub.example.com and deep.sub.example.com.

Allowlisted hosts bypass DNS validation entirely.

Protected Endpoints

Every outbound call from auth, the notify providers, and the AI module is routed through safeFetch() (or, where the client owns its own connection, assertUrlAllowed()):

FileWhat it protects
src/auth/drivers/oauth2.tsOAuth2 token exchange + userinfo fetch
src/auth/drivers/openid.tsOIDC discovery + token exchange + userinfo
src/modules/notify/providers/mattermost.tsMattermost webhook delivery
src/modules/notify/providers/infobip-transport.tsInfobip email API delivery
src/modules/ai/providers/llm/openai-compatible.tsLLM provider (OpenAI-compatible) requests
src/modules/ai/providers/kb/kaos-client.tsKnowledge-base (KAOS) client requests
src/modules/ai/runtime/tools.tsAI tool HTTP fetches
src/modules/ai/runtime/registry.tsAI runtime tool calls (URL may come from chat input)
src/modules/ai/services/integrations.service.tsAI provider integration checks (/models)
src/modules/ai/runtime/mcp-client.tsMCP server URL vetting via assertUrlAllowed() before connect

Error Handling

When a request is blocked:

  1. SSRFBlockedError is thrown (HTTP 403, code SSRF_BLOCKED)
  2. A SSRF_BLOCKED security event is emitted (severity: critical)
  3. The blocked URL and reason are logged via pino
json
{
  "errors": [{
    "message": "SSRF blocked: DNS resolved to private IP: 10.0.0.1 (https://evil.com/redirect)",
    "extensions": { "code": "SSRF_BLOCKED" }
  }]
}

Configuration

VariableTypeDefaultDescription
SSRF_PROTECTION_ENABLEDbooleantrueMaster toggle
SSRF_ALLOWED_HOSTSstring''Comma-separated allowlist

When SSRF_PROTECTION_ENABLED=false, safeFetch() delegates directly to native fetch().

Docker / Kubernetes Note

Internal hostnames (e.g., postgres, redis, backend) in Docker/K8s networks resolve to private IPs and will be blocked. This is intentional for user-controlled URLs (OAuth callbacks, webhooks from admin settings).

For future system-level internal calls (e.g., webhook engine calling internal services), safeFetch will support an allowInternal option to bypass private IP checks on trusted system calls.

Source Files

FilePurpose
src/security/ssrf/safe-fetch.tssafeFetch(), assertUrlAllowed() + SSRFBlockedError
src/security/ssrf/ip-validator.tsisPrivateIP() + isAllowedHost()
src/security/ssrf/dns-cache.tsDNS resolution cache

ODP Internal API Documentation