A10:2021 – Server-Side Request Forgery is a vulnerability where attackers trick servers into unintended requests to internal or external resources, risking data exposure and unauthorized access. It happens when apps mishandle user-supplied URLs. To prevent SSRF, enforce strict URL validation and access controls.

Example

Here I will show you an example of a service that is exposed to the world, and a service which is exposed internally on the same network range without any firewalls or authentication.

The public service

const express = require("express");

const app = express();
const port = 3000;

app.get("/auth", async (req, res) => {
  // Fetch URL from the internal system http://localhost:3001/admin
  // curl http://www.safe.corp/auth?url=http://localhost:3001/admin
  const url = req.query.url;

  try {
    const response = await fetch(url);
    const data = await response.text();

    res.send(data);
  } catch (error) {
    console.error(error);

    res.status(500).send("Error occurred while fetching data.");
  }
});

app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

The internal service not exposed to the Internet.

const express = require("express");

const app = express();
const port = 3001;

app.get("/admin", async (_, res) => {
  res.status(200).send({
    data: "Sensitive breached data",
  });
});

app.listen(port, () => {
  console.log(`Server is running on port ${port}`);
});

The response after targeting the internal URL through the public

http http://www.safe.corp/auth?url=http://localhost:3001/admin
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 64
Content-Type: text/html; charset=utf-8
X-Powered-By: Express

{
    "data": "Sensitive breached data"
}

This is a straight forward example. There are many ways you can do a SSRF attack, and this barely scratches the surface.

The flow

ssrf_flow.png

Prevention

How could this be prevented in the example? It really depends on the purpose of the application, but let us assume we only should allow using the same public host as a url for fetching data from the public application, we could patch it like this:

The patch

app.get("/auth", async (req, res) => {
  // Fetch URL from the internal system http://localhost:3001/admin
  // curl http://www.safe.corp/auth?url=http://localhost:3001/admin
  const url = req.query.url;

  // START PATCH
  const isSafeHost = new URL(req.query.url)?.host === "safe.corp";

  if (!isSafeHost) {
    res.status(400).send("Invalid URL");

    return;
  }
  // END PATCH

  try {
    const response = await fetch(url);
    const data = await response.text();

    res.send(data);
  } catch (error) {
    console.error(error);

    res.status(500).send("Error occurred while fetching data.");
  }
});

The patch response

http http://www.safe.corp/auth?url=http://localhost:3001/admin
HTTP/1.1 400 Bad Request
Connection: keep-alive
Content-Length: 11
Content-Type: text/html; charset=utf-8
Keep-Alive: timeout=5
X-Powered-By: Express

Invalid URL

Resources

https://cheatsheetseries.owasp.org/cheatsheets/Server_Side_Request_Forgery_Prevention_Cheat_Sheet.html

https://owasp.org/www-community/attacks/Server_Side_Request_Forgery

https://portswigger.net/web-security/ssrf

https://github.com/bjarneo/owasp