Server-side request forgery (SSRF)

Validate the destination of server-side requests

Description

SSRF occurs when a server makes a request using a supplied URL or similar input without sufficiently checking that the destination is authorized.

Potential impact

  • Disclosure of sensitive information reachable by the server
  • Unauthorized operations or code execution through exposed services or additional vulnerabilities
  • Access to internal networks or bypass of controls that trust the server's network position

Remediation

  1. Where possible, accept a server-owned endpoint identifier instead of a URL and map it to a fixed URI.
  2. Restrict the server's outbound connections to required destinations. Block access to internal or metadata addresses that the application does not need.
  3. Explicitly disable redirects so the HTTP client cannot follow an approved endpoint to an internal address or another host.

Examples

Before

java
RestTemplate restTemplate = new RestTemplate();
String url = request.getParameter("url");
String result = restTemplate.getForObject(url, String.class);

After

In this Java 11 or later example, replace the URIs with actual administrator-approved destinations. DNS and outbound routing must also be trusted. Enforce a separate deadline for consuming the streaming response body.

java
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;

public final class SafeEndpointClient {
    private static final Map<String, URI> ENDPOINTS = Map.of(
            "profile", URI.create("https://trusted.com/api/profile"),
            "status", URI.create("https://example.com/api/status"));

    private static final HttpClient CLIENT = HttpClient.newBuilder()
            .connectTimeout(Duration.ofSeconds(3))
            .followRedirects(HttpClient.Redirect.NEVER)
            .build();

    public String fetch(HttpServletRequest request) throws IOException, InterruptedException {
        URI target = ENDPOINTS.get(request.getParameter("endpoint"));
        if (target == null) {
            throw new IllegalArgumentException("Unknown endpoint");
        }

        HttpRequest outbound = HttpRequest.newBuilder(target)
                .timeout(Duration.ofSeconds(5))
                .GET()
                .build();
        HttpResponse<InputStream> response =
                CLIENT.send(outbound, HttpResponse.BodyHandlers.ofInputStream());
        if (response.statusCode() >= 300 && response.statusCode() < 400) {
            response.body().close();
            throw new IOException("Redirects are not allowed");
        }

        try (InputStream body = response.body()) {
            byte[] data = body.readNBytes(1024 * 1024 + 1);
            if (data.length > 1024 * 1024) {
                throw new IOException("Response is too large");
            }
            return new String(data, StandardCharsets.UTF_8);
        }
    }
}

The first example requests the client's URL directly. The second lets input select only an endpoint key; the URI comes from a fixed mapping and redirects are disabled. It also sets connection and request timeouts and a response-size limit.

Related CVEs

References