flawopen.com/ssrf/Kotlin

● CWE-918 · 높음
보안 연구 · FlawOpen

취약점 분석: Server-Side Request Forgery in Kotlin

How HTTP clients in Kotlin/Ktor allow SSRF, and how to enforce IP boundaries.

💡 알기 쉬운 설명 (ELI5)

사무실 배달원에게 일반 상점에서 소포를 찾아오라고 하면서, 사장실 안의 잠긴 금고 주소를 적어주는 상황을 상상해 보세요. 배달원은 사내 출입증을 소지하고 있으므로 경비원을 통과해 금고를 열고 회사 기밀을 넘겨주게 됩니다.

핵심 개념 및 용어

Web Application Security
보안 개념 (Web Application Security): Core architecture component affected by CWE-918.
CWE-918
보안 개념 (CWE-918): Standard Common Weakness Enumeration classification for ssrf-kotlin.
Defense-in-Depth
보안 개념 (Defense-in-Depth): Multi-layered engineering verification and runtime boundary isolation.

단계별 공격 실행 흐름

Step 1

신뢰할 수 없는 원격 URL 수신

애플리케이션이 웹훅 전송, 이미지 미리보기 등을 위해 사용자가 제출한 URL을 수신합니다.

Step 2

내부 네트워크 및 클라우드 메타데이터 주소 지정

공격자가 루프백 주소나 클라우드 메타데이터 URL(예: http://169.254.169.254/latest/meta-data/)을 지정합니다.

Step 3

제한 없는 내부 소켓 발신

서버의 HTTP 클라이언트가 사설 VPC 내부에서 IP 범위 검증 없이 요청을 전송합니다.

Step 4

클라우드 자격 증명 및 토큰 유출

내부 메타데이터 서비스가 요청을 신뢰하여 임시 IAM 보안 자격 증명 및 관리자 데이터를 반환합니다.

소스 코드 비교: 취약한 구현 vs 보안 패치

✕ 취약한 구현
// VULNERABLE: Naive java.net.URL openStream()
import java.net.URL;
import java.io.InputStream;
import javax.servlet.http.*;

public class ProxyServlet extends HttpServlet {
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        String targetUrl = req.getParameter("url");
        // Attacker input: http://169.254.169.254/latest/meta-data/
        try {
            URL url = new URL(targetUrl);
            InputStream in = url.openStream();
            in.transferTo(resp.getOutputStream());
        } catch (Exception e) {
            resp.setStatus(500);
        }
    }
}
✓ 보안 강화 패치
// HARDENED: Resolve all IPs, assert non-private, and disable redirects
import java.net.*;
import java.net.http.*;
import java.time.Duration;
import javax.servlet.http.*;

public class ProxyServlet extends HttpServlet {
    private final HttpClient client = HttpClient.newBuilder()
        .connectTimeout(Duration.ofSeconds(3))
        .followRedirects(HttpClient.Redirect.NEVER)
        .build();

    private boolean isSafeAddress(InetAddress addr) {
        return !addr.isLoopbackAddress() &&
               !addr.isSiteLocalAddress() &&
               !addr.isLinkLocalAddress() &&
               !addr.isAnyLocalAddress();
    }

    protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
        String targetUrl = req.getParameter("url");
        try {
            URI uri = URI.create(targetUrl);
            if (!uri.getScheme().equalsIgnoreCase("http") && !uri.getScheme().equalsIgnoreCase("https")) {
                resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid scheme");
                return;
            }

            // 1. Resolve and validate all IP addresses
            InetAddress[] addresses = InetAddress.getAllByName(uri.getHost());
            for (InetAddress addr : addresses) {
                if (!isSafeAddress(addr)) {
                    resp.sendError(HttpServletResponse.SC_FORBIDDEN, "Access to internal IP prohibited");
                    return;
                }
            }

            // 2. Fetch using hardened client
            HttpRequest request = HttpRequest.newBuilder(uri)
                .timeout(Duration.ofSeconds(4))
                .GET()
                .build();

            HttpResponse<byte[]> response = client.send(request, HttpResponse.BodyHandlers.ofByteArray());
            resp.getOutputStream().write(response.body());

        } catch (Exception e) {
            resp.setStatus(HttpServletResponse.SC_FORBIDDEN);
        }
    }
}

엔지니어링 및 시스템 보안 강화 체크리스트

References