OculusCyber Logo

OculusCyber

Home

Browse Topics


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:

  1. Allow only pre-approved domains (exact hostnames or domain suffixes).
  2. Resolve hostname to IP and reject private / loopback / link-local / 169.254/metadata / IPv6 local addresses.
  3. Disable automatic redirects, set short timeouts, and cap response size.
  4. Prefer a proxy/gateway that enforces egress rules.
  5. 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?