Server Side Request Forgery
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
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://owasp.org/www-community/attacks/Server_Side_Request_Forgery