package com.madeu.config; import java.io.IOException; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.core.io.Resource; import org.springframework.util.StringUtils; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.resource.PathResourceResolver; @Configuration public class WebConfig implements WebMvcConfigurer { @Value("${file.upload-path}") private String uploadPath; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/cdn/**") .addResourceLocations("file:" + uploadPath + "/") .setCachePeriod(3600) .resourceChain(true) .addResolver(new PathResourceResolver() { @Override protected Resource getResource(String resourcePath, Resource location) throws IOException { // 1. URL 디코딩 String decodedPath = decodeResourcePath(resourcePath); // 2. 경로 정규화 (보안 검증) if (!isValidPath(decodedPath)) { return null; } // 3. 리소스 찾기 Resource requestedResource = location.createRelative(decodedPath); // 4. 존재 여부 및 허용된 파일 타입 검증 if (requestedResource.exists() && requestedResource.isReadable() && isAllowedResource(requestedResource)) { return requestedResource; } return null; } private String decodeResourcePath(String path) { try { // UTF-8로 URL 디코딩 return URLDecoder.decode(path, StandardCharsets.UTF_8.name()); } catch (Exception e) { // 디코딩 실패 시 원본 반환 return path; } } private boolean isValidPath(String path) { // 경로 탐색 공격 방지 (../ 등) if (path.contains("..") || path.contains("./")) { return false; } // 절대 경로 방지 if (path.startsWith("/") || path.contains(":")) { return false; } return true; } private boolean isAllowedResource(Resource resource) { try { String filename = resource.getFilename(); if (!StringUtils.hasText(filename)) { return false; } // 허용된 확장자 체크 String lowerFilename = filename.toLowerCase(); return lowerFilename.endsWith(".jpg") || lowerFilename.endsWith(".jpeg") || lowerFilename.endsWith(".png") || lowerFilename.endsWith(".gif") || lowerFilename.endsWith(".webp") || lowerFilename.endsWith(".bmp") || lowerFilename.endsWith(".svg"); } catch (Exception e) { return false; } } }); } }