Server‑Side Request Forgery (SSRF)
By Oculus
•
November 9, 2025
You trusting user-supplied URLs? That's trash. SSRF lets attackers make your server talk to internal services, cloud metadata endpoints, or arbitrary hosts. Below: a dangerous (bad) Java example, then a real (good) fix — whitelist + hostname/IP checks + timeouts + no redirects + size limits — with explanations and a defensive checklist you will actually use in production.
Bad example — vulnerable to SSRF
// BAD: naive fetch of user-provided URL — attacker controls destination
@WebServlet("/fetch")
public class FetchServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String url = req.getParameter("url"); // attacker-controlled
// No validation — direct request
URL u = new URL(url);
HttpURLConnection conn = (HttpURLConnection) u.openConnection();
conn.setRequestMethod("GET");
try (InputStream in = conn.getInputStream();
BufferedReader r = new BufferedReader(new InputStreamReader(in))) {
String line;
while ((line = r.readLine()) != null) {
resp.getWriter().println(line);
}
}
}
}
Why this is broken
- Attacker supplies url → can request http://169.254.169.254/latest/meta-data/ (cloud metadata), internal admin endpoints, or internal DB services.
- No hostname or IP validation, no DNS resolution checks, no redirect prevention, no timeouts, no response-size limit.
✅ Good example — robust SSRF mitigation (whitelist + IP checks + hardening)
Key ideas:
- Allow only pre-approved domains (exact hostnames or domain suffixes).
- Resolve hostname to IP and reject private / loopback / link-local / 169.254/metadata / IPv6 local addresses.
- Disable automatic redirects, set short timeouts, and cap response size.
- Prefer a proxy/gateway that enforces egress rules.
- Log & alert on blocked attempts.
Utility + Servlet (example)
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.*;
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.regex.Pattern;
@WebServlet("/safeFetch")
public class SafeFetchServlet extends HttpServlet {
// Allowlist of domains (exact or suffix)
private static final List<String> ALLOWED_DOMAINS = List.of(
"trusted.example.com",
".static-content.example-cdn.com" // allowing subdomains via suffix
);
// Block known bad / sensitive IP ranges using checks (we'll test via InetAddress)
private static final int MAX_RESPONSE_BYTES = 1024 * 64; // 64KB
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String rawUrl = req.getParameter("url");
if (rawUrl == null || rawUrl.length() > 2000) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "url required");
return;
}
URL url;
try {
url = new URL(rawUrl);
} catch (MalformedURLException e) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "invalid url");
return;
}
// 1) Only allow http/https
String protocol = url.getProtocol();
if (!"http".equalsIgnoreCase(protocol) && !"https".equalsIgnoreCase(protocol)) {
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "unsupported protocol");
return;
}
String host = url.getHost();
if (!isDomainAllowed(host)) {
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "domain not allowed");
return;
}
// 2) Resolve and check IP (defend against DNS rebinding)
InetAddress[] addrs;
try {
addrs = InetAddress.getAllByName(host);
} catch (UnknownHostException e) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "hostname cannot be resolved");
return;
}
for (InetAddress addr : addrs) {
if (isPrivateOrLocalAddress(addr)) {
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "host resolves to disallowed IP");
return;
}
}
// 3) Make connection with timeouts, no redirects, and response size cap
HttpURLConnection conn = (HttpURLConnection) url.openConnection(Proxy.NO_PROXY);
conn.setInstanceFollowRedirects(false); // disallow redirects
conn.setConnectTimeout(3_000); // 3s connect
conn.setReadTimeout(5_000); // 5s read
conn.setRequestProperty("User-Agent", "SafeFetcher/1.0");
int status = conn.getResponseCode();
if (status >= 300 && status < 400) {
// Redirect attempt is suspicious — block or follow with extra checks
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "redirects not allowed");
return;
}
try (InputStream in = conn.getInputStream()) {
byte[] buffer = new byte[8192];
int bytesRead;
int total = 0;
OutputStream out = resp.getOutputStream();
while ((bytesRead = in.read(buffer)) != -1) {
total += bytesRead;
if (total > MAX_RESPONSE_BYTES) {
resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "response too large");
return;
}
out.write(buffer, 0, bytesRead);
}
} catch (IOException ex) {
resp.sendError(HttpServletResponse.SC_BAD_GATEWAY, "failed to fetch resource");
} finally {
conn.disconnect();
}
}
private static boolean isDomainAllowed(String host) {
if (host == null) return false;
String lower = host.toLowerCase(Locale.ROOT);
for (String allowed : ALLOWED_DOMAINS) {
if (allowed.startsWith(".")) {
// suffix wildcard
if (lower.endsWith(allowed)) return true;
} else {
if (lower.equals(allowed)) return true;
}
}
return false;
}
private static boolean isPrivateOrLocalAddress(InetAddress addr) {
if (addr.isAnyLocalAddress() || addr.isLoopbackAddress() || addr.isLinkLocalAddress()) return true;
// IPv4 private ranges and IPv6 unique/local addresses
// Use InetAddress methods and explicit checks for 169.254.x.x
String ip = addr.getHostAddress();
if (ip == null) return true;
// quick textual checks for common dangerous ranges
if (ip.startsWith("10.") || ip.startsWith("192.168.") || ip.startsWith("172.")) {
// need proper 172.16.0.0/12 check, so minimal example below:
if (ip.startsWith("172.")) {
String[] oct = ip.split("\\.");
if (oct.length >= 2) {
int second = Integer.parseInt(oct[1]);
if (second >= 16 && second <= 31) return true;
}
} else {
return true;
}
}
if (ip.startsWith("169.254.") || ip.equals("127.0.0.1") || ip.equals("::1")) return true;
// You should expand checks to cover RFC1918, IPv6 unique local ranges (fc00::/7), etc.
return false;
}
}
Why this is better
- Domain allowlist prevents arbitrary hosts.
- DNS resolution + IP checks prevent DNS rebinding to internal IPs.
- Disables redirects to avoid redirect-to-internal-host attacks.
- Short timeouts + response-size cap reduce DoS and large-data exfiltration.
- Prefer Proxy/egress gateway so network-level firewall can enforce whitelists too.
Extra enterprise controls (do these, no excuses)
- Route outbound requests through a hardened proxy/gateway that enforces domain/IP whitelists and logs requests.
- Block access to cloud metadata endpoints (169.254.169.254) at network ACLs and WAF.
- Use strict egress rules in VPC/subnet firewalls — deny all outbound, allow specific destinations.
- Monitor & alert on unusual outbound patterns and blocked requests.
- Use a library or framework that centralizes URL-fetching rules (don't copy-paste ad-hoc checks everywhere).
Quick checklist to verify your code (when code reviewing)
- Are user-supplied URLs disallowed by default?
- Is there a server-side allowlist (domains or IP ranges)?
- Do you resolve hostnames and check IP addresses for private/local ranges?
- Are redirects disabled or strictly validated?
- Are timeouts and response-size limits applied?
- Are outgoing requests forced through a proxy/gateway with egress filtering?
- Is sensitive metadata/network address space blocked at the network level?
- Are requests logged and suspicious attempts alerted?
