flawopen.com/Server-Side Request Forgery/Kotlin

Server-Side Request Forgery in Kotlin

High Severity CWE-918
ELI5

Your Java server is asked to download an invoice logo. The link is 'http://169.254.169.254/secret'. The Java program connects, downloads your cloud keys, and sends them back to the user.

Key terms on this page
InetAddress.getAllByName
Resolves all IP addresses associated with a host name, preventing multi-homed IP evasion.
isSiteLocalAddress
Returns true if the InetAddress is an RFC 1918 private address.

What's happening

In Java, new URL(url).openStream() or naive HttpClient usage fetches arbitrary network destinations. Internal services like Actuator endpoints (/actuator/env) or IMDS are exposed to callers.

Real-world impact

In 2021, severe SSRF vulnerabilities in enterprise Java middleware (Oracle WebLogic and Apache Struts) allowed unauthenticated attackers to query internal database listeners.

CISA Cybersecurity Advisory & MITRE CVE repository.

Vulnerable vs. fixed

VULNERABLE
// VULNERABLE: Naive java.net.URL openStream()
import java.net.URL;
import java.io.InputStream;
import javax.servlet.http.*;

public class ProxyServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        String targetUrl = req.getParameter("url");
        // Attacker input: http://169.254.169.254/latest/meta-data/
        try {
            URL url = new URL(targetUrl);
            InputStream in = url.openStream();
            in.transferTo(resp.getOutputStream());
        } catch (Exception e) {
            resp.setStatus(500);
        }
    }
}
FIXED
// HARDENED: Resolve all IPs, assert non-private, and disable redirects
import java.net.*;
import java.net.http.*;
import java.time.Duration;
import javax.servlet.http.*;

public class ProxyServlet extends HttpServlet {
    private final HttpClient client = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(3))
        .followRedirects(HttpClient.Redirect.NEVER)
        .build();

    private boolean isSafeAddress(InetAddress addr) {
        return !addr.isLoopbackAddress() &&
               !addr.isSiteLocalAddress() &&
               !addr.isLinkLocalAddress() &&
               !addr.isAnyLocalAddress();
    }

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        String targetUrl = req.getParameter("url");
        try {
            URI uri = URI.create(targetUrl);
            if (!uri.getScheme().equalsIgnoreCase("http") && !uri.getScheme().equalsIgnoreCase("https")) {
                resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid scheme");
                return;
            }

            // 1. Resolve and validate all IP addresses
            InetAddress[] addresses = InetAddress.getAllByName(uri.getHost());
            for (InetAddress addr : addresses) {
                if (!isSafeAddress(addr)) {
                    resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Access to internal IP prohibited");
                    return;
                }
            }

            // 2. Fetch using hardened client
            HttpRequest request = HttpRequest.newBuilder(uri)
                .timeout(Duration.ofSeconds(4))
                .GET()
                .build();

            HttpResponse<byte[]> response = client.send(request, HttpResponse.BodyHandlers.ofByteArray());
            resp.getOutputStream().write(response.body());

        } catch (Exception e) {
            resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
        }
    }
}

Why the fix works

The code uses InetAddress.getAllByName() to inspect all IPs associated with the host. isSiteLocalAddress() and isLinkLocalAddress() block internal VPC subnets and cloud metadata. followRedirects(Redirect.NEVER) blocks 302 bypasses.

Gotchas

URL.equals() blocking DNS bug

Java's java.net.URL.equals() triggers a blocking DNS lookup. Always use java.net.URI for representation and validation.

Common misconceptions

"Allowlisting domain names prevents SSRF"

If the allowed domain permits open redirects, or if DNS records point to internal addresses, the check is bypassed.

How to check if you're affected

spotbugs -textui -include spotbugs-security.xml .

Prevention checklist

FAQ

Why is IMDS link-local in Java?

Link-local (169.254.0.0/16) is flagged by addr.isLinkLocalAddress(). Blocking this range protects against AWS, Azure, and GCP metadata exfiltration.

References