Description
Server-side request forgery (SSRF) occurs when user input controls the destination of an outgoing server request. An attacker may make the server contact internal addresses or protected services, such as a cloud metadata endpoint, to discover resources, obtain sensitive information or bypass access boundaries. Redirects, DNS rebinding and open redirects can also undermine destination checks.
Potential impact
- The server may reach internal hosts behind network access controls on the attacker's behalf.
- Internal APIs, database administration services or metadata endpoints may expose credentials or tokens.
- Calls to internal management services may bypass intended access restrictions.
- Repeated requests may exhaust application resources or overload backend services.
- The server may be used to probe internal hosts and ports.
Remediation
- Select hosts from fixed values or a predefined allow-list. Prefer mapping service identifiers to fixed URLs.
- Restrict paths, reject directory traversal such as
.., and construct URLs with a standard parser such asnew URL. - Block destinations that should not be reachable, including loopback, private and link-local ranges such as
127.0.0.0/8,10.0.0.0/8,169.254.0.0/16,172.16.0.0/12and192.168.0.0/16, plus metadata endpoints. - Allow only required HTTP or HTTPS destinations; reject protocols such as
file://,ftp://andgopher://. - Disable automatic redirects or apply the same destination checks to every redirect.
Examples
Before
javascript
// Request the supplied URL directly, risking SSRF
const express = require("express");
const axios = require("axios");
const app = express();
app.get("/proxy", async (req, res) => {
try {
const url = req.query.url; // For example: http://127.0.0.1:8080/admin
if (!url) return res.status(400).send("url required");
// Use the user-supplied URL directly
const resp = await axios.get(url, { timeout: 5000 });
res.send(resp.data);
} catch (e) {
res.status(502).send("bad gateway");
}
});
app.listen(3000);
After
javascript
// Map identifiers to fixed URLs and restrict paths
const express = require("express");
const axios = require("axios");
const app = express();
// 1) Map allowed service identifiers to fixed base URLs
const SERVICE_BASE = new Map([
["status", new URL("https://status.example.com")],
["api", new URL("https://api.example.com")],
]);
// 2) Require one initial '/' and allow only alphanumerics, /, - and _
// Reject protocol-relative URLs such as '//evil.example' and '..' paths.
const SAFE_PATH = /^\/(?!\/)[A-Za-z0-9_\/-]*$/;
app.get("/proxy-safe", async (req, res) => {
try {
const { service, path = "/" } = req.query;
// Validate the service identifier against the allow-list
if (typeof service !== "string" || typeof path !== "string") {
return res.status(400).send("invalid parameters");
}
const base = SERVICE_BASE.get(service);
if (!base) return res.status(400).send("invalid service");
// Validate and normalize the path
if (!SAFE_PATH.test(path) || path.includes("..")) {
return res.status(400).send("invalid path");
}
// Use the standard URL parser to construct the destination
const target = new URL(path, base);
if (target.origin !== base.origin) {
return res.status(400).send("invalid target");
}
// Limit redirects and set a timeout
const resp = await axios.get(target.toString(), {
timeout: 4000,
maxRedirects: 0, // Prevent redirect-based destination changes
responseType: "arraybuffer",
maxContentLength: 1024 * 1024,
validateStatus: (s) => s < 400,
});
// Do not execute remote content as HTML under this application's origin
res.status(resp.status);
res.set("Content-Type", "application/octet-stream");
res.set("X-Content-Type-Options", "nosniff");
res.send(Buffer.from(resp.data));
} catch (e) {
// Handle redirect or validation failures without exposing details
res.status(502).send("bad gateway");
}
});
app.listen(3000);
Explanation:
- Before: The supplied URL controls the server's request, potentially reaching internal or metadata addresses. Following redirects may also move a request from an external to an internal destination.
- After: A server-owned
Mapsupplies the fixed host. Paths must begin with one/and cannot contain protocol-relative destinations such as//evil.exampleor..traversal. - The parsed result must retain the base origin, and
maxRedirects: 0prevents redirect-based destination changes. - The response is limited to 1 MiB and returned as binary data with
application/octet-streamandnosniff, rather than executable HTML under the application's origin. - Restricting user-selected hosts reduces the SSRF attack surface. Also maintain trusted DNS, actual connection destinations and network egress policy for the allowed hosts.