From e564c579d2bf1edaa847d6fca1e12629af6a490b Mon Sep 17 00:00:00 2001 From: bd091 Date: Sun, 7 Dec 2025 00:50:54 +0900 Subject: [PATCH] =?UTF-8?q?=ED=95=9C=EA=B8=80=20=EB=94=94=EC=BD=94?= =?UTF-8?q?=EB=94=A9=20=EB=AC=B8=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/madeu/config/WebConfig.java | 73 +++++++++++++++---- 1 file changed, 58 insertions(+), 15 deletions(-) diff --git a/src/main/java/com/madeu/config/WebConfig.java b/src/main/java/com/madeu/config/WebConfig.java index 3d63e51..6a422a8 100644 --- a/src/main/java/com/madeu/config/WebConfig.java +++ b/src/main/java/com/madeu/config/WebConfig.java @@ -1,14 +1,17 @@ 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; -import java.io.IOException; - @Configuration public class WebConfig implements WebMvcConfigurer { @@ -19,32 +22,72 @@ public class WebConfig implements WebMvcConfigurer { public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/cdn/**") .addResourceLocations("file:" + uploadPath + "/") - .setCachePeriod(3600) // 1시간 캐싱 + .setCachePeriod(3600) .resourceChain(true) .addResolver(new PathResourceResolver() { @Override protected Resource getResource(String resourcePath, Resource location) throws IOException { - Resource requestedResource = location.createRelative(resourcePath); + // 1. URL 디코딩 + String decodedPath = decodeResourcePath(resourcePath); - // 보안 검증: 허용된 파일 타입만 - if (requestedResource.exists() && requestedResource.isReadable() - && isAllowedResource(requestedResource)) { + // 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(); - return filename != null && - (filename.toLowerCase().endsWith(".jpg") || - filename.toLowerCase().endsWith(".jpeg") || - filename.toLowerCase().endsWith(".png") || - filename.toLowerCase().endsWith(".gif") || - filename.toLowerCase().endsWith(".webp") || - filename.toLowerCase().endsWith(".bmp") || - filename.toLowerCase().endsWith(".svg")); + 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; }