Appearance
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.tsalso exportsassertUrlAllowed(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 whenSSRF_PROTECTION_ENABLED=falseand throwsSSRFBlockedErrorotherwise.
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
| Range | Description |
|---|---|
127.0.0.0/8 | Loopback |
10.0.0.0/8 | Private (RFC1918) |
172.16.0.0/12 | Private (RFC1918) |
192.168.0.0/16 | Private (RFC1918) |
169.254.0.0/16 | Link-local (cloud metadata) |
0.0.0.0/8 | "This" network |
::1 | IPv6 loopback |
fe80::/10 | IPv6 link-local |
fc00::/7 | IPv6 unique local |
::ffff:x.x.x.x | IPv4-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.comWildcard 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()):
| File | What it protects |
|---|---|
src/auth/drivers/oauth2.ts | OAuth2 token exchange + userinfo fetch |
src/auth/drivers/openid.ts | OIDC discovery + token exchange + userinfo |
src/modules/notify/providers/mattermost.ts | Mattermost webhook delivery |
src/modules/notify/providers/infobip-transport.ts | Infobip email API delivery |
src/modules/ai/providers/llm/openai-compatible.ts | LLM provider (OpenAI-compatible) requests |
src/modules/ai/providers/kb/kaos-client.ts | Knowledge-base (KAOS) client requests |
src/modules/ai/runtime/tools.ts | AI tool HTTP fetches |
src/modules/ai/runtime/registry.ts | AI runtime tool calls (URL may come from chat input) |
src/modules/ai/services/integrations.service.ts | AI provider integration checks (/models) |
src/modules/ai/runtime/mcp-client.ts | MCP server URL vetting via assertUrlAllowed() before connect |
Error Handling
When a request is blocked:
SSRFBlockedErroris thrown (HTTP 403, codeSSRF_BLOCKED)- A
SSRF_BLOCKEDsecurity event is emitted (severity:critical) - 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
| Variable | Type | Default | Description |
|---|---|---|---|
SSRF_PROTECTION_ENABLED | boolean | true | Master toggle |
SSRF_ALLOWED_HOSTS | string | '' | 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
| File | Purpose |
|---|---|
src/security/ssrf/safe-fetch.ts | safeFetch(), assertUrlAllowed() + SSRFBlockedError |
src/security/ssrf/ip-validator.ts | isPrivateIP() + isAllowedHost() |
src/security/ssrf/dns-cache.ts | DNS resolution cache |