Back to Security Academy
INJECTION DEFENSESADVANCED10 min readCWE-918ZX-SSRF-001

Defeating Server-Side Request Forgery in Webhooks & Data Fetchers

How unvalidated backend HTTP fetch calls expose internal cloud metadata (169.254.169.254) and private networks, and how to implement socket-level IP pinning.

Why AI coding tools introduce this vulnerability

AI tools frequently implement features like "URL unfurling", "Webhook testing", or "Avatar image proxy" using simple fetch(userUrl). An attacker inputs http://169.254.169.254/latest/meta-data/iam/security-credentials/ or http://localhost:5432, allowing them to steal cloud IAM credentials or query internal databases.

Technical Vulnerability Analysis

Server-Side Request Forgery occurs when a web application fetches a remote resource without validating the destination IP address or protecting against DNS rebinding. Attackers exploit SSRF to pivot from the public web into private VPCs, Kubernetes metadata APIs, and internal Redis/PostgreSQL instances.

VULNERABLE IMPLEMENTATION (DO NOT USE)
// AI-Generated Webhook Dispatcher (VULNERABLE TO SSRF)
export async function testWebhook(webhookUrl: string) {
  // Directly fetches user-supplied URL!
  // Attacker inputs: http://169.254.169.254/latest/meta-data/
  const res = await fetch(webhookUrl);
  return res.text();
}
No DNS resolution checks, no IP blacklists, and no protection against private network pivoting.

Remediation Playbook & Code Fixes

SECURE HARDENED PATCH (src/core/net/safe-fetch.ts)
import dns from 'dns/promises';
import net from 'net';

function isPrivateIp(ip: string): boolean {
  // Loopback (127.0.0.0/8, ::1)
  if (ip.startsWith('127.') || ip === '::1') return true;
  // RFC 1918 Private Ranges (10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16)
  if (ip.startsWith('10.') || ip.startsWith('192.168.')) return true;
  // Cloud Metadata (169.254.169.254)
  if (ip === '169.254.169.254') return true;
  return false;
}

export async function safeFetch(targetUrl: string): Promise<Response> {
  const parsed = new URL(targetUrl);
  if (!['http:', 'https:'].includes(parsed.protocol)) {
    throw new Error('Unsupported protocol');
  }

  // Pre-flight DNS resolution
  const addresses = await dns.lookup(parsed.hostname, { all: true });
  for (const { address } of addresses) {
    if (isPrivateIp(address)) {
      throw new Error(`SSRF Defense: IP ${address} is a restricted private address`);
    }
  }

  return fetch(targetUrl, { redirect: 'error' }); // Avoid redirect bypasses
}

Why this fix works: Validate resolved IP against private CIDR ranges prior to initiating socket connection.

Local CLI Verification Commands

Verify cloud metadata IP is rejected with 400 Bad Request.
curl -s -X POST https://example.com/api/webhooks/test -d '{"url":"http://169.254.169.254/latest/meta-data/"}'
Expected result: {"error":"SSRF Defense: IP is a restricted private address"}

Pre-Deployment Self-Audit Checklist

Knowledge Check: Verify Your Understanding

What is the primary architectural cause of defeating server-side request forgery in webhooks & data fetchers?

Test your website for ZX-SSRF-001

Run our 14 deterministic engines to inspect your live deployment.

Launch scan for this flaw