전자금융기반시설 보안 취약점 평가기준 - WEB-SER-046

서버 사이드 요청 위조 (SSRF) | 위험도: 4 (높음) | 통제구분: 5.8.4 서비스 보호

OO은행

자산관리 포털 - 대출상품 이미지 조회 서비스

취약점: URL 파라미터 검증 미흡

상품 이미지 프록시 서비스

외부 마케팅 이미지를 서버에서 대신 다운로드하여 표시합니다

SSRF 공격 시나리오

취약한 Java 코드

위험
// ❌ 취약한 코드 - URL 검증 없음
@RestController
public class ImageProxyController {
    
    @GetMapping("/api/proxy/image")
    public ResponseEntity<byte[]> getImage(
        @RequestParam String url) {
        
        // URL 검증 없이 바로 요청!
        RestTemplate restTemplate = new RestTemplate();
        byte[] imageBytes = restTemplate
            .getForObject(url, byte[].class);
        
        return ResponseEntity.ok(imageBytes);
    }
}

서버 응답

IDLE
> Waiting for request...

안전한 Java 코드

권장
// ✅ 안전한 코드 - 화이트리스트 검증
@RestController
public class SecureImageProxyController {
    
    // 허용된 도메인 목록
    private static final Set<String> ALLOWED_DOMAINS = 
        Set.of("cdn.shinhan.com", "images.shinhan.com");
    
    // 차단할 IP 대역 (내부망, 메타데이터)
    private static final Set<String> BLOCKED_IPS = Set.of(
        "127.0.0.1", "localhost", 
        "169.254.169.254", "metadata.google.internal",
        "10.", "172.16.", "192.168."
    );
    
    @GetMapping("/api/proxy/image")
    public ResponseEntity<byte[]> getImage(
        @RequestParam String url) throws Exception {
        
        // 1. URL 형식 검증
        if (!url.startsWith("https://")) {
            throw new IllegalArgumentException(
                "HTTPS만 허용됩니다");
        }
        
        // 2. 도메인 화이트리스트 검증
        URL urlObj = new URL(url);
        String host = urlObj.getHost();
        
        boolean isAllowed = ALLOWED_DOMAINS.stream()
            .anyMatch(domain -> host.equals(domain) || 
                      host.endsWith("." + domain));
        
        if (!isAllowed) {
            throw new SecurityException(
                "허용되지 않은 도메인: " + host);
        }
        
        // 3. IP 주소로 변환하여 내부망 체크
        InetAddress address = InetAddress.getByName(host);
        String ip = address.getHostAddress();
        
        // 사설 IP 대역 차단
        if (address.isSiteLocalAddress() || 
            address.isLoopbackAddress() ||
            address.isLinkLocalAddress()) {
            throw new SecurityException(
                "내부 IP 접근 차단: " + ip);
        }
        
        // 추가 IP 블랙리스트 체크
        for (String blocked : BLOCKED_IPS) {
            if (ip.startsWith(blocked)) {
                throw new SecurityException(
                    "차단된 IP: " + ip);
            }
        }
        
        // 4. Timeout 설정 (5초)
        RestTemplate restTemplate = new RestTemplate();
        SimpleClientHttpRequestFactory factory = 
            new SimpleClientHttpRequestFactory();
        factory.setConnectTimeout(5000);
        factory.setReadTimeout(5000);
        restTemplate.setRequestFactory(factory);
        
        // 5. 안전하게 요청
        byte[] imageBytes = restTemplate
            .getForObject(url, byte[].class);
        
        // 6. 응답 크기 제한 (10MB)
        if (imageBytes.length > 10 * 1024 * 1024) {
            throw new IllegalArgumentException(
                "이미지 크기 초과");
        }
        
        return ResponseEntity.ok(imageBytes);
    }
}

SSRF 방어 체크리스트

도메인 화이트리스트: 허용된 도메인만 접근
내부 IP 차단: 127.0.0.1, 10.x, 192.168.x 차단
메타데이터 차단: 169.254.169.254 차단
프로토콜 제한: HTTPS만 허용
Timeout 설정: 5초 제한