서버 사이드 요청 위조 (SSRF) | 위험도: 4 (높음) | 통제구분: 5.8.4 서비스 보호
자산관리 포털 - 대출상품 이미지 조회 서비스
외부 마케팅 이미지를 서버에서 대신 다운로드하여 표시합니다
// ❌ 취약한 코드 - 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);
}
}
// ✅ 안전한 코드 - 화이트리스트 검증
@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);
}
}