説明
ユーザー入力をクエリ文字列にそのまま連結すると、区切り文字や重複するパラメーターが追加され、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
- CVE-2022-36069: Pythonの依存関係管理ツールで、Gitコマンドの生成時にオプション引数を注入できた事例です。HTTPのHPPとは異なる、コマンドライン引数の注入です。
- CVE-2024-4084: mintplex-labs/anything-llmのSSRF