Skip to content
Lesson 7 of 8

SSRF and Vulnerable Components

9 min read

Server-Side Request Forgery (SSRF)

SSRF (Server-Side Request Forgery, CWE-918) is a vulnerability that gained so much relevance that the OWASP Top 10 dedicates its own category to it (A10:2021). It occurs when an application fetches a remote resource from a user-supplied URL without validating it. The attacker gets the server to make requests on their behalf, using the server's privileged position within the network.

What makes SSRF so dangerous is that the server usually has access to internal resources the attacker cannot reach directly: services on the internal network, databases, admin panels, or, in cloud environments, the metadata endpoints that expose the instance's temporary credentials. An SSRF can thus become the gateway to the internal network or to the theft of cloud credentials, escalating from a seemingly harmless parameter to a serious infrastructure compromise.

The real impact: cloud metadata

In a cloud provider, every instance has a metadata service reachable only from within the instance itself, at a reserved link-local IP address. That service returns configuration information and, in many cases, temporary credentials tied to the instance's role. If a function vulnerable to SSRF lets the attacker point a server-side request at that internal endpoint, they can read those credentials and act with the instance's permissions over other resources in the account.

One of the largest cloud data breaches of recent years followed exactly this pattern: an SSRF reached the metadata service, obtained instance credentials, and used them to access stored data belonging to millions of people. You do not need to memorize the case; what matters is understanding the chain: unvalidated URL input → server-side request to an internal resource → privileged access. Breaking any link in that chain mitigates the risk.

Example: Vulnerable SSRF and Its Fix

Consider an endpoint that downloads an image from a user-supplied URL. The naive version trusts the input completely:

// VULNERABLE: the user's URL is used without any validation
import express from "express";

const app = express();

app.get("/fetch-image", async (req, res) => {
  const { url } = req.query;
  const response = await fetch(url); // the server fetches whatever it is given
  const body = await response.arrayBuffer();
  res.type("image/png").send(Buffer.from(body));
});

The problem is that url may point to an internal service, to localhost, or to the metadata endpoint. The fix applies defense in depth: an allow-list of domains, resolution and validation of the destination IP, protocol restriction, and blocking of automatic redirects.

// SECURE: domain allow-list, IP and protocol validation, no redirects
import express from "express";
import dns from "node:dns/promises";
import net from "node:net";
import ipaddr from "ipaddr.js";

const app = express();

const ALLOWED_HOSTS = new Set(["images.example.com", "cdn.example.com"]);
const ALLOWED_PROTOCOLS = new Set(["http:", "https:"]);

async function isSafeUrl(rawUrl) {
  let parsed;
  try {
    parsed = new URL(rawUrl);
  } catch {
    return false; // input that is not even a valid URL
  }

  // 1. Only http/https
  if (!ALLOWED_PROTOCOLS.has(parsed.protocol)) return false;

  // 2. Strict domain allow-list
  if (!ALLOWED_HOSTS.has(parsed.hostname)) return false;

  // 3. Resolve the IP and block internal/loopback ranges
  const { address } = await dns.lookup(parsed.hostname);
  if (!net.isIP(address)) return false;
  const range = ipaddr.parse(address).range();
  const blocked = ["private", "loopback", "linkLocal", "uniqueLocal", "reserved"];
  if (blocked.includes(range)) return false;

  return true;
}

app.get("/fetch-image", async (req, res) => {
  const { url } = req.query;

  if (typeof url !== "string" || !(await isSafeUrl(url))) {
    return res.status(400).json({ error: "URL not allowed" });
  }

  const response = await fetch(url, { redirect: "manual" }); // do not follow redirects
  const body = await response.arrayBuffer();
  res.type("image/png").send(Buffer.from(body));
});

Each layer covers a gap in the previous one: the allow-list limits where the request can go, IP resolution prevents an allowed-but-compromised domain or a DNS trick from pointing at an internal range, and redirect: "manual" stops an external server from redirecting the request toward localhost or the metadata service.

How to Prevent SSRF

The most robust defense against SSRF is to never trust user-supplied URLs for server-side requests. When the functionality requires it, validate the URL with a strict allow-list: only explicitly authorized domains and paths, instead of trying to block the dangerous ones with a deny-list, which can always be bypassed.

As additional layers, resolve and validate the destination IP address to block internal and loopback ranges, disable automatic redirects or validate them as well, and restrict the allowed protocols to http and https. At the network level, apply segmentation and outbound firewall rules so that, even if the SSRF occurs, the server cannot reach sensitive internal services. In the cloud, use the latest, hardened version of the metadata service. The combination of input validation and network segmentation is the appropriate defense in depth.

Insecure Deserialization

Insecure deserialization (CWE-502) occurs when an application reconstructs objects from serialized data that comes from an untrusted source. Serialization converts objects into a transmittable format; deserialization reconstructs them. If an attacker can manipulate the serialized data, in some languages and libraries they can manipulate which objects are created and which methods are invoked during the reconstruction process.

The impact can reach remote code execution on the server, one of the worst possible scenarios. A classic example is using Python's pickle on data that arrives from the network or the client:

# VULNERABLE: pickle reconstructs arbitrary objects from untrusted data
import pickle

def load_session(raw_bytes):
    # raw_bytes comes from a cookie, a message, or an uploaded file
    return pickle.loads(raw_bytes)  # can execute code during deserialization

pickle is not designed for untrusted data: during reconstruction it can invoke behavior defined by the data itself. The fix is to use a pure data format with a strict schema, one that only produces known structures (dictionaries, lists, numbers, and strings):

# SECURE: JSON produces only data, plus strict schema validation
import json
from pydantic import BaseModel, ValidationError

class Session(BaseModel):
    user_id: int
    role: str
    expires_at: int

def load_session(raw_text):
    try:
        data = json.loads(raw_text)      # never instantiates arbitrary objects
        return Session(**data)           # validates expected types and fields
    except (json.JSONDecodeError, ValidationError):
        raise ValueError("Invalid session")

The primary prevention is to avoid deserializing untrusted data whenever possible. When you need to exchange data, prefer formats like JSON with strict schemas, instead of the language's native object serialization. If you truly must deserialize objects, cryptographically sign the data to verify its integrity before processing it, restrict the types that can be deserialized to a known list, and run the process with the least possible privileges.

Vulnerable Components and Dependencies

Modern applications are built on hundreds of third-party libraries. The OWASP Top 10 dedicates category A06:2021 — Vulnerable and Outdated Components to this risk, and it is enormous precisely because of its scale: a single vulnerable dependency, possibly one you did not even know you had (a transitive dependency), can compromise the entire application. Historical incidents in widely used frameworks and libraries affected thousands of organizations simultaneously, many of which did not even know the affected component was part of their dependency chain.

The problem is aggravated because dependencies become outdated silently. A library that was secure when you added it may have a vulnerability discovered months later. Without a process to detect and update these components, you accumulate invisible security debt: third-party code that no one is watching and that an attacker actively reviews looking for known, vulnerable versions. Client-side vulnerabilities also fit here, where third-party scripts loaded on your page can be compromised and directly affect your users' browsers.

Supply Chain Security

Supply chain security consists of managing the risk introduced by all the external components your software depends on. The first step is visibility: keep an up-to-date inventory of all your dependencies, ideally generating an SBOM (Software Bill of Materials) that lists each component and its version, including transitive dependencies.

On that basis, integrate software composition analysis (SCA) into your development pipeline: tools that compare your dependencies against databases of known vulnerabilities (CVE) and alert you when a new one appears. Automate security updates, pin versions to avoid unexpected changes, verify the integrity of the packages you download, and reduce your surface by removing dependencies you do not use.

Detection Tools

  • npm audit (and its equivalents pip-audit, cargo audit): a quick review of known vulnerabilities in the dependency tree, straight from the command line.
  • Dependabot / Renovate: automate detection of vulnerable versions and open pull requests with the updates, integrated into the repository.
  • Snyk and OWASP Dependency-Check: more complete SCA tools that analyze dependencies, licenses, and exploitation paths, suitable for CI/CD integration.
  • Burp Suite and OWASP ZAP: for dynamically testing the application's behavior, including validating the defenses against SSRF.
  • SAST/DAST: static analysis reviews your code for dangerous patterns (for example, a user URL reaching a request without validation), and dynamic analysis tests the running application.

Your application is only as secure as its weakest component, even if that component was written by someone else.

Prevention Checklist

  • SSRF: validate every user URL against an allow-list of domains; never use deny-lists as the sole defense.
  • Resolve the destination IP and block internal, loopback, and link-local ranges before making the request.
  • Restrict protocols to http/https and disable automatic redirects or validate them as well.
  • Apply network segmentation and outbound firewalling: the server should not be able to reach sensitive internal services.
  • In the cloud, use the hardened version of the metadata service and limit the instance role's permissions to the minimum.
  • Deserialization: avoid deserializing untrusted data; prefer JSON with a strict schema over native object serialization.
  • If you must deserialize objects, sign and verify integrity, restrict the allowed types, and run with least privilege.
  • Components: generate an SBOM and run SCA on every build; keep dependencies up to date.
  • Pin versions, verify package integrity, and remove dependencies you do not use.
  • Automate alerts with npm audit, Dependabot, or Snyk and treat them as part of the workflow, not an occasional task.