HTTP Parameter Pollution (HPP) 및 SSRF

HTTP Parameter Pollution

설명

사용자 입력을 쿼리 문자열에 그대로 연결하면 구분자나 중복 매개변수가 추가되어 HTTP Parameter Pollution (HPP) 이 발생할 수 있습니다. 매개변수 해석 방식에 따라 요청의 의미가 달라집니다.

Server-Side Request Forgery (SSRF) 는 공격자가 서버의 요청 대상을 제어하는 별개의 문제입니다. 고정된 호스트의 쿼리를 변경하는 것만으로 요청 대상이 바뀌지는 않지만, 수신 API가 해당 매개변수를 다른 요청의 URL로 사용하면 SSRF로 이어질 수 있습니다.

잠재적 영향

  • 공격자가 기존 요청 파라미터를 덮어써 권한이 없는 기능을 수행하도록 유도할 수 있습니다.
  • SSRF로 이어지면 내부 API 또는 클라우드 메타데이터 서비스(http://169.254.169.254/)에 접근할 위험이 있습니다.
  • 그 결과 내부 네트워크의 민감한 정보가 유출될 수 있습니다.

해결 방법

  • 사용자 입력을 URL 또는 URL 매개변수에 직접 포함하지 않도록 적용하세요.
  • Lookup Table을 사용하여 미리 정의된 값만 허용하도록 하세요.
  • 쿼리 매개변수 값에 맞는 인코딩(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;
        }

        // Lookup Table을 사용하여 허용된 값만 전달
        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

참조