We reach the last category of the Top Ten and the second of the 2021 newcomers (alongside A04 Insecure Design). A10:2021 – Server-Side Request Forgery (SSRF) made the list driven by the community survey and by its growing relevance in cloud environments. The idea is as simple as it is dangerous: if a feature makes the server perform an HTTP (or other) request to a URL that the user controls, an attacker can make the server request things they couldn't request directly: internal services, admin panels, and —most coveted in the cloud— the metadata service that exposes the instance's credentials.

We already brushed against SSRF in the XXE lesson (03-07), where an external entity made the XML parser go fetch a URL. Here we treat it generally and with its canonical example in BazarNube: a function that downloads an image from a URL provided by the user (for example, to import a profile picture or a product image from a link). Done well it's convenient; done poorly, it's a door into the internal network.

Legal and ethical notice: the examples of access to internal services and metadata are illustrative and use fake data. Practice only on systems you own or have explicit authorization to test. Reaching someone else's internal network or metadata without permission is a crime.

Contents

  1. What SSRF is and why it's so dangerous in the cloud
  2. The BazarNube image-download case
  3. How it's exploited (illustrative)
  4. Prevention 1: destination allowlist
  5. Prevention 2: block redirects and internal addresses
  6. Prevention 3: network segmentation and defense in depth
  7. Common mistakes, exercises and solutions

  1. What SSRF is and why it's so dangerous in the cloud

In an SSRF, the attacker doesn't attack the internal service directly; they use the server as an intermediary. The server is usually in a privileged network position: it can reach databases, queues, admin panels and the cloud provider's metadata endpoint, all of them normally unreachable from the internet. By controlling the destination URL, the attacker "borrows" that position.

The star target in the cloud is the metadata service (a link-local IP, of the type 169.254.169.254), which can return the instance's temporary credentials. With them, an SSRF can escalate to a full compromise of the cloud account. That's why A10 jumped into the Top Ten.

  1. The BazarNube image-download case

Marc built a function to import a product image from a URL the seller enters:

// media.js — VULNERABLE to SSRF
router.post('/api/products/:id/image-from-url', requireLogin, async (req, res) => {
  const { imageUrl } = req.body;                 // user-controlled URL
  const response = await fetch(imageUrl);         // the SERVER makes the request
  const buffer = Buffer.from(await response.arrayBuffer());
  await saveProductImage(req.params.id, buffer);
  res.json({ ok: true });
});

The server takes imageUrl as-is and does a fetch wherever the user says. The destination isn't validated, nor the scheme, nor whether the URL points to the internal network. It's textbook SSRF.

  1. How it's exploited (illustrative)

Instead of an image, the attacker sends internal URLs:

POST /api/products/42/image-from-url
{ "imageUrl": "http://169.254.169.254/latest/meta-data/iam/security-credentials/" }

The server, with its privileged access, queries the metadata service and (depending on the configuration) obtains the instance's credentials, which may end up stored or reflected. Other variants:

http://10.0.0.5:5432/           -> probe the internal database
http://localhost:9200/          -> an internal service (e.g. a search engine) with no auth
file:///etc/passwd              -> read local files (if the client follows file://)
http://internal/admin           -> an admin panel not exposed to the internet
flowchart LR
  A[Attacker] -->|internal imageUrl| B[BazarNube API]
  B -->|the server requests it| C[Cloud metadata / internal service]
  C -->|response with sensitive data| B
  B -->|reflected or stored| A

  1. Prevention 1: destination allowlist

The most robust defense is an allowlist: define exactly which destinations the function may reach and reject everything else. A blocklist ("block 169.254.169.254") is insufficient, because there are countless ways to obfuscate addresses (decimal, hexadecimal, DNS that resolves to an internal IP, IPv6...).

// media.js — SECURE (part 1): scheme and host allowlist
const ALLOWED_HOSTS = new Set(['cdn.image-provider.com', 'images.bazarnube.com']);

function assertAllowedUrl(raw) {
  const url = new URL(raw);                          // throws if it's not a valid URL
  if (!['https:'].includes(url.protocol))            // HTTPS only
    throw new Error('Scheme not allowed');
  if (!ALLOWED_HOSTS.has(url.hostname))              // only hosts on the allowlist
    throw new Error('Host not allowed');
  return url;
}

If the use case allows (it often does), the best is not to accept arbitrary URLs at all: have the user upload the file directly, or pick from a catalog of allowed providers. Removing the "download from a free URL" feature eliminates the class of vulnerability.

  1. Prevention 2: block redirects and internal addresses

A host allowlist can be bypassed in two ways that must be closed:

  • DNS rebinding / host resolving to an internal IP: the attacker controls a domain (cdn.evil.example) that resolves to 169.254.169.254. That's why you must also validate the resolved IP, not just the name.
  • Redirects: an allowed host responds with a 302 to an internal URL. That's why you must disable automatic redirect following (or re-validate each hop).
// media.js — SECURE (part 2): resolve IP, reject internal ranges, no redirects
const dns = require('dns').promises;
const ipaddr = require('ipaddr.js');

async function assertPublicIp(hostname) {
  const { address } = await dns.lookup(hostname);   // the real IP it resolves to
  const addr = ipaddr.parse(address);
  const range = addr.range();                        // 'private', 'loopback', 'linkLocal'...
  if (['private', 'loopback', 'linkLocal', 'uniqueLocal', 'reserved'].includes(range))
    throw new Error('Internal destination not allowed');  // blocks 10.x, 127.x, 169.254.x...
}

router.post('/api/products/:id/image-from-url', requireLogin, async (req, res) => {
  try {
    const url = assertAllowedUrl(req.body.imageUrl);
    await assertPublicIp(url.hostname);              // validate the resolved IP
    const response = await fetch(url, { redirect: 'error', signal: timeout(5000) }); // no redirects, with timeout
    if (!response.ok) return res.status(400).json({ error: 'Download failed' });
    const type = response.headers.get('content-type') || '';
    if (!type.startsWith('image/')) return res.status(400).json({ error: 'Not an image' });
    const buffer = Buffer.from(await response.arrayBuffer());
    await saveProductImage(req.params.id, buffer);
    res.json({ ok: true });
  } catch (e) {
    logger.warn({ event: 'ssrf_blocked', reqId: req.id, reason: e.message }); // links to A09
    res.status(400).json({ error: 'URL not allowed' });
  }
});

Key details: redirect: 'error' stops an allowed host from redirecting to an internal one; assertPublicIp rejects any internal-range IP (including the metadata link-local 169.254.x); the timeout limits abuse; validating the content-type as an image adds a barrier; and the blocked event is logged (A09).

Note on the TOCTOU race: between resolving the IP and doing the fetch, DNS could change. In high-risk scenarios, resolve the IP and connect to that validated IP (pinning the host), or use an egress proxy that enforces the policy centrally.

  1. Prevention 3: network segmentation and defense in depth

Even if the code is perfect, the infrastructure should limit the damage:

Layer Control
Application Host/scheme allowlist, validate resolved IP, no redirects, timeouts
Network Segment: the service shouldn't be able to reach the internal network or the metadata
Cloud Use the hardened version of the metadata service (which requires a token) and least-privilege roles
Egress An egress proxy that centralizes and enforces the allowed-destination policy
Observability Log and alert on anomalous outbound requests (A09)

The underlying rule, again, is least privilege: if the function only needs to reach a public CDN, the network should physically prevent it from reaching 169.254.169.254 or 10.0.0.0/8. Defense in depth ensures a failure in one layer isn't catastrophic.

Common Mistakes and Tips

  • Accepting arbitrary user URLs. If you can avoid it (direct upload, catalog), eliminate the class of vulnerability.
  • Using an IP blocklist. It's evadable (encodings, DNS to an internal IP). Use a host allowlist and validate the resolved IP.
  • Following redirects automatically. An allowed host can redirect to an internal one; use redirect: 'error'.
  • Validating only the hostname, not the IP. DNS rebinding bypasses it; resolve and validate the range.
  • Forgetting the cloud metadata. It's the priority target; block it at the network level and use the token-requiring variant.
  • Not setting timeouts. They enable abuse and port scanning via timing.
  • Tip: combine control in the application (allowlist + IP validation), in the network (segmentation) and in the cloud (hardened metadata + least privilege). No single layer is enough on its own.

Exercises

Exercise 1. Why is a blocklist that blocks 169.254.169.254 insufficient to prevent SSRF? Give two ways to evade it.

Exercise 2. The team adds a host allowlist but still follows redirects. Describe how an attacker could bypass the allowlist and how it's fixed.

Exercise 3. Propose a redesign of the "image from URL" function that eliminates the SSRF class of vulnerability instead of just mitigating it.

Solutions

Solution 1. Because there are many ways to refer to the same address or to reach internal destinations without using that exact string: (a) alternative encodings of the IP (decimal 2852039166, hexadecimal, IPv6, 0177.0.0.1...); (b) a domain controlled by the attacker that resolves via DNS to an internal IP (rebinding). The blocklist doesn't cover these variants; that's why you use an allowlist plus validation of the actually resolved IP.

Solution 2. The attacker uses an allowed host (or one they control that the allowlist accepts) that responds with a 302 Location: http://169.254.169.254/...; if the client follows the redirect, it ends up at the internal destination. It's fixed with redirect: 'error' (or manual, re-validating each hop with the same allowlist + IP policy).

Solution 3. Replace the "download from a free URL" with a direct file upload by the user (multipart) or a selection from a closed catalog of allowed providers/CDNs. If the server never makes a request to a user-controlled URL, there's no SSRF to mitigate: the class of vulnerability disappears by design (links to A04).

Conclusion

SSRF turns the server into an unwitting accomplice that requests resources on the attacker's behalf, and in the cloud it can escalate all the way to the instance's credentials. It's prevented with a destination allowlist, validation of the resolved IP, blocking redirects, timeouts, and —above all— network segmentation and hardened metadata. The best mitigation, when feasible, is not to accept arbitrary URLs and redesign the feature.

Backlog entry — A10: image-from-URL function secured with a host/scheme allowlist, resolved-IP validation, redirect: 'error' and a timeout; network segmentation to block access to metadata and internal IPs; cloud metadata in hardened mode; blocked SSRF events logged in the SIEM.


Module 3 wrap-up. We've covered the ten categories of the OWASP Top Ten 2021 —A01 to A10— plus the two inherited deep dives (XSS and XXE), and in each we followed the same cycle on BazarNube: vulnerable code → illustrative attack → fixed code → backlog entry. The Top Ten gave us an excellent map of the most frequent risks, but that's exactly what it is: the "ten most frequent", not an exhaustive checklist verifiable requirement by requirement.

To go from "I know the major risks" to "I can verify systematically and by levels that my application meets a complete catalog of security requirements", we need a more detailed and testable standard. That's the natural leap of module 4: OWASP ASVS (Application Security Verification Standard) in depth, where we'll turn this module's lessons into verifiable requirements organized by assurance levels. Many of the fixes we noted in BazarNube's backlog will reappear there as concrete ASVS requirements we'll know how to verify.

OWASP Course: Guidelines and Standards for Web Application Security

Module 1: Introduction to OWASP

Module 2: Main OWASP Projects

Module 3: OWASP Top Ten 2021 in Depth

Module 4: OWASP ASVS (Application Security Verification Standard)

Module 5: OWASP SAMM (Software Assurance Maturity Model)

Module 6: OWASP ZAP (Zed Attack Proxy)

Module 7: Best Practices and Recommendations

Module 8: Practical Exercises and Case Studies

Module 9: Assessment and Certification

© Copyright 2026. All rights reserved