서버 사이드 요청 위조 (SSRF)

서버가 요청할 목적지를 검증하여 SSRF 방지

설명

웹 서버에서 주어진 URL 등을 바탕으로 요청을 실행할 때, 승인된 목적지인지 충분히 검증하지 않음으로 인해 발생합니다.

잠재적 영향

  • 데이터 탈취 (Confidentiality): 공격자는 민감한 데이터에 접근할 수 있습니다.
  • 내부 서비스 오용: 접근 가능한 서비스의 기능이나 추가 취약점에 따라 무단 작업이나 코드 실행으로 이어질 수 있습니다.
  • 보호 메커니즘 우회 (Access Control): 공격자는 서버를 이용해 방화벽을 우회하거나 내부 네트워크를 스캔할 수 있는 등 보안 보호책을 우회할 수 있습니다.

해결 방법

  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을 바로 사용하기 때문에 악의적인 사용자 또는 공격자가 서버 내부 네트워크에 접근하거나 보호 메커니즘을 우회할 수 있습니다.
  • 변경 후: 외부 입력은 URL이 아니라 서버가 관리하는 엔드포인트 키만 선택합니다. 실제 URI는 상수 매핑에서 가져오고 JDK HTTP 클라이언트의 리다이렉트를 비활성화하므로 사용자가 목적지나 리다이렉트 대상을 지배할 수 없습니다. 연결·요청 시간과 응답 크기도 제한합니다.

관련 CVE

참조