HTTPパラメーター汚染(HPP)とSSRF

HTTPパラメーター汚染

説明

ユーザー入力をクエリ文字列にそのまま連結すると、区切り文字や重複するパラメーターが追加され、HTTPパラメーター汚染(HPP) が発生するおそれがあります。受信側のパラメーターの解釈によって、リクエストの意味が変わります。

サーバーサイドリクエストフォージェリ(SSRF) は、攻撃者がサーバーのリクエスト先を制御する別の問題です。ホストが固定されたURLのクエリを変更しても、それだけでリクエスト先は変わりません。ただし、受信側のAPIがその値を別のリクエストのURLとして使用すると、SSRFにつながる可能性があります。

想定される影響

  • 既存のパラメーターが上書きされ、権限のない操作を実行されるおそれがあります。
  • SSRFにつながる場合、内部APIやクラウドのメタデータサービス(http://169.254.169.254/)にアクセスされるおそれがあります。
  • その結果、内部ネットワークの機密情報が漏えいする可能性があります。

対処方法

  • 未処理のユーザー入力をURLやそのパラメーターに直接連結しないでください。
  • 選択肢が決まっている場合は、対応表で定義済みの値だけを許可します。
  • java.net.URLEncoder.encode などでクエリパラメーターの値を適切にエンコードします。
    • 選択肢が固定されている場合は許可リストを使います。デコードした値を別のURLにそのまま連結しないでください。エンコードだけでは接続先の信頼性を検証できません。
  • ユーザーが指定した任意のURL全体をリクエスト先として受け入れないでください。

例

変更前

java
import java.io.IOException;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.HttpClients;

public class UnsafeHttpRequestServlet extends HttpServlet {
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String userInput = request.getParameter("url"); // ユーザー入力を使用
        // 問題のあるURL連結(入力例: 1&url=169.254.169.254)
        String targetUrl = "https://example.com/api?url=api.example.com&query=" + userInput;

        try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
            HttpGet httpGet = new HttpGet(targetUrl);
            try (CloseableHttpResponse clientResponse = httpClient.execute(httpGet)) {
                // レスポンスを処理
            }
        }
    }
}

変更後

java
import java.io.IOException;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.impl.classic.HttpClients;

public class SafeHttpRequestServlet extends HttpServlet {
    private static final Map<String, String> allowedValues = new HashMap<>();

    static {
        allowedValues.put("item1", "value1");
        allowedValues.put("item2", "value2");
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String userInput = request.getParameter("key");
        if (userInput == null) {
            response.sendError(HttpServletResponse.SC_BAD_REQUEST);
            return;
        }

        // 対応表で許可された値だけを渡す
        String safeValue = allowedValues.getOrDefault(userInput, "value1");

        try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
            HttpGet httpGet = new HttpGet("https://example.com/api?query=" + safeValue);
            try (CloseableHttpResponse clientResponse = httpClient.execute(httpGet)) {
                // レスポンスを処理
            }
        }

        // 別の方法: クエリ値だけをエンコードします。デコード後に別のURLへそのまま連結しないでください。
        String encodedString = java.net.URLEncoder.encode(userInput, StandardCharsets.UTF_8);
        final HttpGet httpget = new HttpGet("https://example.com/getId?key=" + encodedString);

    }
}

説明:

  • 変更前: 入力の &url=... によってパラメーターが重複する可能性があります。SSRFになるかどうかは、受信側のAPIがその値をリクエスト先に使うかによります。
  • 変更後: 許可リストの値からリクエストを組み立てます。その後の別の例はクエリ値のエンコード方法を示しており、リクエストは実行しません。

関連するCVE

参考資料