サーバーサイドリクエストフォージェリ(SSRF)

サーバーから送るリクエストの接続先を検証する

説明

サーバーが指定されたURLなどを使ってリクエストを送る際、許可された接続先か十分に確認しないとSSRFが発生します。

想定される影響

  • サーバーから到達できる機密情報の漏えい
  • 接続先の機能や別の脆弱性を利用した不正な操作・コード実行
  • 内部ネットワークへのアクセスや、サーバーの接続元を信頼する保護機構の回避

対処方法

  1. 可能ならURLを直接受け取らず、サーバーが管理するエンドポイントIDを固定URIに対応付けてください。
  2. サーバーからの送信を必要な接続先に限定し、不要な内部アドレスやメタデータサービスへのアクセスを遮断してください。
  3. 許可された接続先から内部アドレスや別ホストへ転送されないよう、HTTPクライアントのリダイレクトを明示的に無効化してください。

例

変更前

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

変更後

Java 11以降の例です。URIは管理者が承認した実際の接続先に置き換えてください。DNSと送信経路も信頼できる必要があります。ストリーミングで返る本文を読み終えるまでの時間制限は別途設けてください。

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);
        }
    }
}

変更前はクライアントが指定したURLへ直接アクセスします。変更後は入力でエンドポイントのキーだけを選び、URIを固定の対応表から取得します。リダイレクトを無効化し、接続・リクエストの時間とレスポンスサイズも制限します。

関連CVE

参考資料