From 201bd9ccbfe38d4185fa902efd933b6aa180d861 Mon Sep 17 00:00:00 2001 From: wooh Date: Fri, 29 May 2026 22:32:53 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[Fix]=20=EA=B2=80=EC=A6=9D=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EC=9D=91=EB=8B=B5=EC=9D=98=20=EB=AF=BC=EA=B0=90=20?= =?UTF-8?q?=EC=A0=95=EB=B3=B4=20=EB=A7=88=EC=8A=A4=ED=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 비밀번호 등 민감 필드가 validation 실패 응답과 로그에 원문 그대로 포함되지 않도록 수정했습니다. ExceptionAdvice에서 rejected value 포맷팅 시 민감 필드 여부를 검사하도록 변경 password, token, secret, authorization, credential 키워드가 포함된 필드는 ****로 마스킹 일반 필드의 입력값 노출 방식은 기존 동작 유지 민감 필드 마스킹 여부를 검증하는 단위 테스트 추가 --- .../exception/handler/ExceptionAdvice.java | 40 ++++++++++++++++--- .../handler/ExceptionAdviceTest.java | 27 +++++++++++++ 2 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 src/test/java/com/jobdri/jobdri_api/global/apiPayload/exception/handler/ExceptionAdviceTest.java diff --git a/src/main/java/com/jobdri/jobdri_api/global/apiPayload/exception/handler/ExceptionAdvice.java b/src/main/java/com/jobdri/jobdri_api/global/apiPayload/exception/handler/ExceptionAdvice.java index 0ea6d0e..813bc68 100644 --- a/src/main/java/com/jobdri/jobdri_api/global/apiPayload/exception/handler/ExceptionAdvice.java +++ b/src/main/java/com/jobdri/jobdri_api/global/apiPayload/exception/handler/ExceptionAdvice.java @@ -16,11 +16,21 @@ import org.springframework.web.bind.annotation.RestControllerAdvice; import java.util.List; +import java.util.Locale; +import java.util.Set; import java.util.stream.Collectors; @Slf4j @RestControllerAdvice(annotations = RestController.class) public class ExceptionAdvice { + private static final String MASKED_VALUE = "****"; + private static final Set SENSITIVE_FIELD_KEYWORDS = Set.of( + "password", + "token", + "secret", + "authorization", + "credential" + ); @ExceptionHandler(GeneralException.class) public ResponseEntity> handleCustomException(GeneralException e) { @@ -37,7 +47,7 @@ public ResponseEntity> handleValidationException(MethodArgum .map(fe -> String.format("[%s] %s (입력값: %s)", fe.getField(), fe.getDefaultMessage(), - fe.getRejectedValue())) + formatRejectedValue(fe.getField(), fe.getRejectedValue()))) .toList(); log.warn("Validation failed: {}", errors); @@ -51,10 +61,13 @@ public ResponseEntity> handleValidationException(MethodArgum @ExceptionHandler(ConstraintViolationException.class) public ResponseEntity> handleConstraintViolationException(ConstraintViolationException e) { List errors = e.getConstraintViolations().stream() - .map(violation -> String.format("[%s] %s (입력값: %s)", - violation.getPropertyPath().toString(), - violation.getMessage(), - violation.getInvalidValue())) + .map(violation -> { + String propertyPath = violation.getPropertyPath().toString(); + return String.format("[%s] %s (입력값: %s)", + propertyPath, + violation.getMessage(), + formatRejectedValue(propertyPath, violation.getInvalidValue())); + }) .collect(Collectors.toList()); log.warn("Constraint violation: {}", errors); @@ -88,4 +101,21 @@ public ResponseEntity> handleException(Exception e) { .status(code.getHttpStatus()) .body(ApiResponse.onFailure(code, code.getMessage())); } + + static String formatRejectedValue(String field, Object rejectedValue) { + if (isSensitiveField(field)) { + return MASKED_VALUE; + } + return String.valueOf(rejectedValue); + } + + static boolean isSensitiveField(String field) { + if (field == null) { + return false; + } + + String normalizedField = field.toLowerCase(Locale.ROOT); + return SENSITIVE_FIELD_KEYWORDS.stream() + .anyMatch(normalizedField::contains); + } } diff --git a/src/test/java/com/jobdri/jobdri_api/global/apiPayload/exception/handler/ExceptionAdviceTest.java b/src/test/java/com/jobdri/jobdri_api/global/apiPayload/exception/handler/ExceptionAdviceTest.java new file mode 100644 index 0000000..3c2ff52 --- /dev/null +++ b/src/test/java/com/jobdri/jobdri_api/global/apiPayload/exception/handler/ExceptionAdviceTest.java @@ -0,0 +1,27 @@ +package com.jobdri.jobdri_api.global.apiPayload.exception.handler; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class ExceptionAdviceTest { + + @Test + @DisplayName("민감한 검증 실패 입력값은 마스킹한다") + void maskSensitiveRejectedValue() { + assertThat(ExceptionAdvice.formatRejectedValue("password", "raw-password")) + .isEqualTo("****"); + assertThat(ExceptionAdvice.formatRejectedValue("loginRequest.password", "raw-password")) + .isEqualTo("****"); + assertThat(ExceptionAdvice.formatRejectedValue("refreshToken", "raw-token")) + .isEqualTo("****"); + } + + @Test + @DisplayName("민감하지 않은 검증 실패 입력값은 기존처럼 노출한다") + void keepNonSensitiveRejectedValue() { + assertThat(ExceptionAdvice.formatRejectedValue("email", "invalid-email")) + .isEqualTo("invalid-email"); + } +} From 9200e7e7ec41be3a4d022d5bde9a5b8e661a4b61 Mon Sep 17 00:00:00 2001 From: wooh Date: Fri, 29 May 2026 22:57:37 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[Fix]=20=EA=B2=80=EC=A6=9D=20=EC=97=90?= =?UTF-8?q?=EB=9F=AC=20=EB=AF=BC=EA=B0=90=EA=B0=92=20=EB=A7=88=EC=8A=A4?= =?UTF-8?q?=ED=82=B9=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EB=B3=B4=EA=B0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 검증 실패 응답에서 민감 필드 입력값이 노출되지 않도록 하는 마스킹 로직의 테스트 범위를 확장했습니다. secret, authorization, credential 키워드 마스킹 검증 추가 PASSWORD, RefreshToken 등 대소문자 혼합 필드명 검증 추가 필드명이 null인 경우 기존 입력값을 유지하는지 검증 입력값이 null인 경우 현재 포맷팅 결과를 명시적으로 검증 민감 필드 입력값이 null이어도 마스킹되는지 검증 --- .../handler/ExceptionAdviceTest.java | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/jobdri/jobdri_api/global/apiPayload/exception/handler/ExceptionAdviceTest.java b/src/test/java/com/jobdri/jobdri_api/global/apiPayload/exception/handler/ExceptionAdviceTest.java index 3c2ff52..8b001d7 100644 --- a/src/test/java/com/jobdri/jobdri_api/global/apiPayload/exception/handler/ExceptionAdviceTest.java +++ b/src/test/java/com/jobdri/jobdri_api/global/apiPayload/exception/handler/ExceptionAdviceTest.java @@ -8,14 +8,29 @@ class ExceptionAdviceTest { @Test - @DisplayName("민감한 검증 실패 입력값은 마스킹한다") - void maskSensitiveRejectedValue() { + @DisplayName("민감한 검증 실패 입력값은 키워드별로 마스킹한다") + void maskAllSensitiveKeywords() { assertThat(ExceptionAdvice.formatRejectedValue("password", "raw-password")) .isEqualTo("****"); assertThat(ExceptionAdvice.formatRejectedValue("loginRequest.password", "raw-password")) .isEqualTo("****"); assertThat(ExceptionAdvice.formatRejectedValue("refreshToken", "raw-token")) .isEqualTo("****"); + assertThat(ExceptionAdvice.formatRejectedValue("clientSecret", "raw-secret")) + .isEqualTo("****"); + assertThat(ExceptionAdvice.formatRejectedValue("authorizationHeader", "raw-authorization")) + .isEqualTo("****"); + assertThat(ExceptionAdvice.formatRejectedValue("credentialKey", "raw-credential")) + .isEqualTo("****"); + } + + @Test + @DisplayName("민감한 검증 실패 입력값은 대소문자와 무관하게 마스킹한다") + void maskCaseInsensitiveFields() { + assertThat(ExceptionAdvice.formatRejectedValue("PASSWORD", "raw-password")) + .isEqualTo("****"); + assertThat(ExceptionAdvice.formatRejectedValue("RefreshToken", "raw-token")) + .isEqualTo("****"); } @Test @@ -24,4 +39,15 @@ void keepNonSensitiveRejectedValue() { assertThat(ExceptionAdvice.formatRejectedValue("email", "invalid-email")) .isEqualTo("invalid-email"); } + + @Test + @DisplayName("검증 실패 입력값 포맷팅 시 null 입력을 처리한다") + void handleNullInputs() { + assertThat(ExceptionAdvice.formatRejectedValue(null, "raw-value")) + .isEqualTo("raw-value"); + assertThat(ExceptionAdvice.formatRejectedValue("email", null)) + .isEqualTo("null"); + assertThat(ExceptionAdvice.formatRejectedValue("password", null)) + .isEqualTo("****"); + } }