SSRF Protection
Server-Side Request Forgery (SSRF) is a security vulnerability where an attacker can trick a server into making requests to internal resources. Fluxbase includes built-in SSRF protection for webhooks and external HTTP requests.
What is SSRF?
Section titled “What is SSRF?”SSRF (Server-Side Request Forgery) allows attackers to:
- Access internal services (databases, admin panels, metadata services)
- Bypass firewalls and access controls
- Scan internal networks
- Access cloud provider metadata services (e.g., AWS IMDSv1)
- Port scan internal infrastructure
Common Attack Scenarios:
// Attacker creates webhook with malicious URLconst webhook = await client.webhook.create({ url: 'http://localhost:6379', // Redis database events: [{ schema: 'public', table: 'users', event: 'INSERT' }]})
// Or access cloud metadataconst webhook = await client.webhook.create({ url: 'http://169.254.169.254/latest/meta-data/iam/security-credentials', // AWS metadata events: [{ schema: 'public', table: 'users', event: 'INSERT' }]})Fluxbase SSRF Protection
Section titled “Fluxbase SSRF Protection”Fluxbase automatically blocks webhook requests to internal resources:
Blocked IP Ranges
Section titled “Blocked IP Ranges”| IP Range | Description | Risk |
|---|---|---|
10.0.0.0/8 | Private network | Internal services |
172.16.0.0/12 | Private network | Internal services |
192.168.0.0/16 | Private network | Internal services |
127.0.0.0/8 | Loopback | Local services |
169.254.0.0/16 | Link-local | AWS metadata endpoint |
::1/128 | IPv6 loopback | Local services |
fc00::/7 | IPv6 unique local | Internal services |
fe80::/10 | IPv6 link-local | Local services |
Blocked Hostnames
Section titled “Blocked Hostnames”| Hostname | Purpose | Protected Service |
|---|---|---|
localhost | Local machine | Local services |
metadata.google.internal | GCP metadata | Cloud credentials |
metadata | AWS metadata (EC2) | Cloud credentials |
instance-data | AWS metadata | Cloud credentials |
kubernetes.default.svc | Kubernetes API | Cluster services |
kubernetes.default | Kubernetes API | Cluster services |
URL Scheme Validation
Section titled “URL Scheme Validation”Only http:// and https:// schemes are allowed:
# ❌ BLOCKED: File protocolurl: "file:///etc/passwd"
# ❌ BLOCKED: FTP protocolurl: "ftp://internal-server.com"
# ❌ BLOCKED: gopher protocolurl: "gopher://internal-host:70"
# ✅ ALLOWED: HTTP/HTTPS onlyurl: "https://api.example.com/webhook"Configuration
Section titled “Configuration”Enable/Disable SSRF Protection
Section titled “Enable/Disable SSRF Protection”Default: SSRF protection is enabled by default.
webhook: # ⚠️ WARNING: Only disable in development/testing allow_private_ips: false # Default: false (SSRF protection enabled)Environment Variable:
export FLUXBASE_WEBHOOK_ALLOW_PRIVATE_IPS=falseDevelopment/Testing
Section titled “Development/Testing”For local development with internal services:
# Development config (NOT for production)webhook: allow_private_ips: true # ⚠️ Only for local development!Never enable allow_private_ips: true in production.
How Protection Works
Section titled “How Protection Works”1. URL Validation
Section titled “1. URL Validation”When a webhook is created or updated, Fluxbase validates the URL:
// Internal validation (automatic)1. Parse URL and check scheme (http/https only)2. Extract hostname3. Check against blocked hostname list4. Resolve hostname to IP addresses5. Check each IP against private IP ranges6. Reject if any check fails2. DNS Resolution
Section titled “2. DNS Resolution”Fluxbase performs DNS resolution with a 5-second timeout:
// DNS lookup with timeoutctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)defer cancel()
resolver := net.Resolver{}ips, err := resolver.LookupIPAddr(ctx, hostname)Protected against:
- DNS rebinding attacks
- Slow DNS attacks (timeout protection)
- DNS spoofing (uses system resolver)
3. IP Address Checks
Section titled “3. IP Address Checks”Each resolved IP is checked against private ranges:
// Check for private IP blocksprivateBlocks := []string{ "10.0.0.0/8", // RFC 1918 "172.16.0.0/12", // RFC 1918 "192.168.0.0/16", // RFC 1918 "169.254.0.0/16", // AWS metadata "127.0.0.0/8", // Loopback "::1/128", // IPv6 loopback "fc00::/7", // IPv6 unique local "fe80::/10", // IPv6 link local}4. Hostname Patterns
Section titled “4. Hostname Patterns”Subdomains of blocked hostnames are also blocked:
metadata.google.internal ❌ BLOCKEDapi.metadata.google.internal ❌ BLOCKED (subdomain)kubernetes.default.svc ❌ BLOCKEDpod.kubernetes.default.svc ❌ BLOCKED (subdomain)Error Messages
Section titled “Error Messages”When SSRF protection blocks a webhook:
{ "error": "URL resolves to private IP address 192.168.1.1 which is not allowed"}{ "error": "localhost URLs are not allowed"}{ "error": "internal hostname 'metadata.google.internal' is not allowed"}{ "error": "URL scheme must be http or https, got: file"}Testing SSRF Protection
Section titled “Testing SSRF Protection”1. Test Valid URLs
Section titled “1. Test Valid URLs”// ✅ Should workconst webhook = await client.webhook.create({ url: 'https://webhook.site/unique-id', events: [{ schema: 'public', table: 'users', event: 'INSERT' }]})2. Test Private IP Blocking
Section titled “2. Test Private IP Blocking”// ❌ Should be blockedtry { await client.webhook.create({ url: 'http://192.168.1.1/webhook', events: [{ schema: 'public', table: 'users', event: 'INSERT' }] })} catch (error) { console.log(error.message) // "URL resolves to private IP address 192.168.1.1 which is not allowed"}3. Test Localhost Blocking
Section titled “3. Test Localhost Blocking”// ❌ Should be blockedtry { await client.webhook.create({ url: 'http://localhost:8080/webhook', events: [{ schema: 'public', table: 'users', event: 'INSERT' }] })} catch (error) { console.log(error.message) // "localhost URLs are not allowed"}4. Test Cloud Metadata Blocking
Section titled “4. Test Cloud Metadata Blocking”// ❌ Should be blockedtry { await client.webhook.create({ url: 'http://169.254.169.254/latest/meta-data/iam/', events: [{ schema: 'public', table: 'users', event: 'INSERT' }] })} catch (error) { console.log(error.message) // "URL resolves to private IP address 169.254.169.254 which is not allowed"}Advanced Protection
Section titled “Advanced Protection”Custom Header Validation
Section titled “Custom Header Validation”Fluxbase also validates custom webhook headers to prevent injection:
// Blocked headers (cannot be overridden)- content-length- host- transfer-encoding- connection- keep-alive- proxy-authenticate- proxy-authorization- te- trailors- upgradeHeader Injection Protection:
// ❌ BLOCKED: CRLF injectionconst webhook = await client.webhook.create({ url: 'https://api.example.com/webhook', headers: { 'X-Custom': 'value\r\nX-Injected: malicious' }})// Error: "header value for 'X-Custom' contains invalid characters"Header Length Limits
Section titled “Header Length Limits”Custom header values are limited to 8192 bytes:
// ❌ BLOCKED: Header too longconst webhook = await client.webhook.create({ url: 'https://api.example.com/webhook', headers: { 'X-Huge': 'a'.repeat(10000) // Too long }})// Error: "header value for 'X-Huge' exceeds maximum length of 8192 bytes"Best Practices
Section titled “Best Practices”1. Use HTTPS for Webhooks
Section titled “1. Use HTTPS for Webhooks”// ✅ GOOD: HTTPS with valid certificateconst webhook = await client.webhook.create({ url: 'https://api.example.com/webhook', events: [...]})
// ⚠️ ACCEPTABLE: HTTP for development onlyconst webhook = await client.webhook.create({ url: 'http://localhost:3000/webhook', // Development only events: [...]})2. Validate Webhook URLs
Section titled “2. Validate Webhook URLs”// Client-side validation before sendingfunction validateWebhookURL(url: string): boolean { try { const parsed = new URL(url) return parsed.protocol === 'https:' || parsed.protocol === 'http:' } catch { return false }}
if (!validateWebhookURL(userInput)) { throw new Error('Invalid webhook URL')}3. Use Allowlists for Production
Section titled “3. Use Allowlists for Production”// Only allow specific domainsconst ALLOWED_WEBHOOK_DOMAINS = [ 'webhook.site', 'api.example.com', 'hooks.your-domain.com']
function isAllowedWebhookURL(url: string): boolean { const parsed = new URL(url) return ALLOWED_WEBHOOK_DOMAINS.some(domain => parsed.hostname === domain || parsed.hostname.endsWith('.' + domain) )}4. Monitor Webhook Failures
Section titled “4. Monitor Webhook Failures”logging: audit_enabled: true audit_events: - "webhook.ssrf_blocked" - "webhook.delivery_failed"5. Rate Limit Webhook Creation
Section titled “5. Rate Limit Webhook Creation”ratelimit: webhook_create_per_minute: 10 webhook_create_per_hour: 50Cloud Provider Considerations
Section titled “Cloud Provider Considerations”AWS (Amazon Web Services)
Section titled “AWS (Amazon Web Services)”Protected:
- EC2 metadata endpoint (
169.254.169.254) - ECS metadata endpoint
- Lambda metadata endpoints
Recommendations:
- Use IMDSv2 (requires session tokens)
- Restrict IAM roles for webhook URLs
- Use VPC endpoints for internal services
Google Cloud Platform
Section titled “Google Cloud Platform”Protected:
- Compute Engine metadata (
metadata.google.internal) - Cloud Run metadata
Recommendations:
- Use service account impersonation
- Restrict service account permissions
Protected:
- Instance Metadata Service (169.254.169.254)
Recommendations:
- Use Managed Identities
- Restrict network access
Troubleshooting
Section titled “Troubleshooting”Webhook Creation Fails with “Private IP” Error
Section titled “Webhook Creation Fails with “Private IP” Error”Issue: Legitimate webhook URL blocked
Solutions:
-
Check DNS resolution:
Terminal window nslookup your-webhook-domain.com -
Verify no CNAME to internal IP:
Terminal window dig your-webhook-domain.com -
Check CDN/proxy configuration:
- Some CDNs resolve to internal IPs
- Use specific CDN edge endpoints
Webhook Works Locally but Fails in Production
Section titled “Webhook Works Locally but Fails in Production”Cause: Localhost only works with allow_private_ips: true
Solution: Use external webhook testing service:
// Development: Use webhook testing serviceconst webhook = await client.webhook.create({ url: 'https://webhook.site/your-unique-id', events: [...]})Security Checklist
Section titled “Security Checklist”- SSRF protection enabled (
allow_private_ips: false) - Webhooks use HTTPS only
- Custom headers validated
- Webhook creation rate limited
- Failed webhook attempts logged
- Cloud metadata endpoints blocked
- Private IP ranges blocked
- Localhost variants blocked
- DNS timeout configured (5 seconds)
- Production webhooks use allowlist