Vulnerability Research
SSRF
Cloud Security
OWASP
Penetration Testing

SSRF Attacks Explained: How Attackers Reach Your Internal Network via Your App

SecureCodeReviews Team
March 1, 2026
13 min read
Share

What is SSRF?

Server-Side Request Forgery (SSRF) is a vulnerability where an attacker can make a server-side application send HTTP requests to an arbitrary destination — including internal services, cloud metadata endpoints, and other systems that should never be accessible from the internet.

SSRF was added to the OWASP Top 10 in 2021 as A10: Server-Side Request Forgery. It's one of the fastest-growing vulnerability categories, driven by cloud adoption and microservice architectures.


How SSRF Works

1. Attacker → Your App: "Fetch this URL: http://169.254.169.254/latest/meta-data/iam/security-credentials/"
2. Your App → Cloud Metadata API: Fetches the URL (server-side)
3. Cloud Metadata → Your App: Returns IAM credentials
4. Your App → Attacker: Forwards the response containing AWS keys

The key insight: your server trusts itself. Internal services, cloud metadata APIs, and admin panels that block external requests happily respond to requests from your own server.


Real-World SSRF Breaches

Capital One (2019) — $80M Fine, 100M Records

The attacker exploited an SSRF vulnerability in a WAF (Web Application Firewall) to access AWS metadata at 169.254.169.254, obtained IAM role credentials, and used them to exfiltrate 100+ million customer records from S3.

Attack chain:

  1. SSRF to http://169.254.169.254/latest/meta-data/iam/security-credentials/
  2. Retrieved temporary AWS credentials
  3. Used aws s3 sync to download all customer data
  4. 100M credit card applications, 140,000 SSNs, 80,000 bank account numbers

GitLab (2021) — Critical SSRF

GitLab's import feature allowed users to specify a URL to import a project from. The URL wasn't properly validated, allowing attackers to:

  • Access internal GitLab services
  • Read AWS/GCP metadata
  • Scan internal network ports

Common SSRF Patterns

// Slack-style link preview
app.post('/api/preview', async (req, res) => {
  const { url } = req.body;
  const response = await fetch(url); // SSRF!
  const html = await response.text();
  const title = html.match(/<title>(.*?)<\/title>/)?.[1];
  res.json({ title, url });
});

Pattern 2: Webhook / Callback URLs

// Payment webhook
app.post('/api/webhooks/configure', async (req, res) => {
  const { callbackUrl } = req.body;
  // Attacker sets callbackUrl to http://169.254.169.254/...
  await fetch(callbackUrl, { method: 'POST', body: testPayload }); // SSRF!
});

Pattern 3: Image / File Processing

// Image resizer that fetches from URL
app.post('/api/resize', async (req, res) => {
  const { imageUrl, width, height } = req.body;
  const image = await fetch(imageUrl); // SSRF!
  const buffer = await image.buffer();
  const resized = await sharp(buffer).resize(width, height).toBuffer();
  res.send(resized);
});

Pattern 4: PDF Generation

// HTML-to-PDF with server-side rendering
app.post('/api/generate-pdf', async (req, res) => {
  const { htmlUrl } = req.body;
  // Puppeteer navigates to the URL server-side
  await page.goto(htmlUrl); // SSRF!
  const pdf = await page.pdf();
  res.send(pdf);
});

SSRF Defense in Depth

Layer 1: URL Validation

const { URL } = require('url');
const dns = require('dns').promises;
const net = require('net');

async function isSafeUrl(urlString) {
  let parsed;
  try {
    parsed = new URL(urlString);
  } catch {
    return false;
  }

  // Block non-HTTP protocols
  if (!['http:', 'https:'].includes(parsed.protocol)) return false;

  // Block known dangerous hostnames
  const blocked = ['localhost', '127.0.0.1', '0.0.0.0', '[::1]', 'metadata.google.internal'];
  if (blocked.includes(parsed.hostname)) return false;

  // Resolve DNS and check actual IP
  const addresses = await dns.resolve4(parsed.hostname).catch(() => []);
  for (const addr of addresses) {
    if (net.isIP(addr) && isPrivateIP(addr)) return false;
  }

  return true;
}

function isPrivateIP(ip) {
  const parts = ip.split('.').map(Number);
  return (
    parts[0] === 10 ||
    parts[0] === 127 ||
    (parts[0] === 172 && parts[1] >= 16 && parts[1] <= 31) ||
    (parts[0] === 192 && parts[1] === 168) ||
    (parts[0] === 169 && parts[1] === 254)
  );
}

Layer 2: AWS IMDSv2 (Block Metadata SSRF)

# Require IMDSv2 (token-based) on all EC2 instances
aws ec2 modify-instance-metadata-options \
  --instance-id i-1234567890 \
  --http-tokens required \
  --http-put-response-hop-limit 1

Layer 3: Network Segmentation

  • Run user-facing applications in a VPC with no direct access to internal services
  • Use security groups to block outbound connections to metadata endpoints
  • Use a proxy/gateway for all outbound requests with URL allowlisting

Layer 4: Response Validation

Even if an SSRF request succeeds, don't return the raw response:

// Only return specific, expected fields
const response = await safeFetch(url);
const data = await response.json();
res.json({
  title: typeof data.title === 'string' ? data.title.slice(0, 200) : '',
  description: typeof data.description === 'string' ? data.description.slice(0, 500) : '',
});

SSRF Testing Checklist

TestMethod
Cloud metadataTry http://169.254.169.254/latest/meta-data/ (AWS), http://metadata.google.internal/ (GCP)
Internal servicesTry http://localhost:PORT for common ports (3000, 5432, 6379, 8080)
DNS rebindingRegister a domain that alternates between public and private IPs
Protocol smugglingTry file:///etc/passwd, gopher://, dict://
Redirect bypassHost a URL that 302-redirects to http://169.254.169.254/

How We Help

Our penetration testing and code review services include comprehensive SSRF testing. We check every server-side URL fetch, webhook handler, and external integration for SSRF vulnerabilities.

Request a Free Sample Code Review → | Schedule a Pentest →

Advertisement