fix(security): GlobalExceptionHandler SQL/스택 노출 제거
This commit is contained in:
parent
616982ffd4
commit
adbf8b6271
@ -1,23 +1,46 @@
|
||||
package com.zioinfo.mall.common;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.ResponseStatus;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.multipart.MaxUploadSizeExceededException;
|
||||
import org.springframework.web.servlet.resource.NoResourceFoundException;
|
||||
|
||||
import java.sql.SQLException;
|
||||
|
||||
/**
|
||||
* 전역 예외 처리.
|
||||
*
|
||||
* <p>보안 불변 규칙: 스택트레이스를 응답에 절대 노출하지 않는다.
|
||||
* 에러 코드 + 요약 메시지만 반환한다.
|
||||
* <p>보안 불변 규칙: 스택트레이스 · SQL문 · 테이블/컬럼명 · DB 드라이버 메시지를
|
||||
* 응답에 절대 노출하지 않는다. 클라이언트에는 에러 코드 + 일반 요약 메시지만 반환하고,
|
||||
* 실제 원인은 서버 로그(logger.error)에만 기록한다.
|
||||
*
|
||||
* <p>업무/검증 예외는 {@code ERR-XXX-NNN:} 형태의 비민감 메시지만 통과시킨다.
|
||||
* 그 외(미분류 RuntimeException, DataAccessException, SQLException, 일반 Exception)는
|
||||
* 메시지를 마스킹하고 일반 코드로만 응답한다.
|
||||
*/
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
/**
|
||||
* 안전한 업무/검증 에러 코드 패턴 (예: ERR-MALL-413, ERR-ORD-404, ERR-LOY-400 ...).
|
||||
* 메시지가 이 코드로 시작하면 사전 정의된 비민감 메시지로 간주하여 그대로 노출한다.
|
||||
* 그렇지 않은 메시지(SQL/드라이버/내부 원인 가능성)는 응답에서 마스킹한다.
|
||||
*/
|
||||
private static final java.util.regex.Pattern SAFE_CODE =
|
||||
java.util.regex.Pattern.compile("^ERR-[A-Z0-9]+-[A-Z0-9]+\\b.*");
|
||||
|
||||
/** 사전 정의된 안전 코드 메시지인지 검사. */
|
||||
private static boolean isSafeBusinessMessage(String message) {
|
||||
return message != null && SAFE_CODE.matcher(message).matches();
|
||||
}
|
||||
|
||||
@ExceptionHandler(MaxUploadSizeExceededException.class)
|
||||
@ResponseStatus(HttpStatus.PAYLOAD_TOO_LARGE)
|
||||
public ApiResponse<Void> handleMaxSize(MaxUploadSizeExceededException e) {
|
||||
@ -28,17 +51,54 @@ public class GlobalExceptionHandler {
|
||||
@ExceptionHandler(IllegalArgumentException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public ApiResponse<Void> handleIllegalArg(IllegalArgumentException e) {
|
||||
// 사전 정의된 비민감 검증 메시지(ERR-XXX-NNN)만 노출, 그 외는 마스킹
|
||||
if (isSafeBusinessMessage(e.getMessage())) {
|
||||
log.warn("잘못된 요청: {}", e.getMessage());
|
||||
return ApiResponse.fail(e.getMessage());
|
||||
}
|
||||
log.warn("잘못된 요청(메시지 마스킹)", e);
|
||||
return ApiResponse.fail("ERR-MALL-400: 잘못된 요청입니다");
|
||||
}
|
||||
|
||||
/**
|
||||
* DB 접근 오류 — SQL문/테이블·컬럼명/드라이버 메시지가 e.getMessage()에 포함되므로
|
||||
* 절대 응답에 노출하지 않는다. 원인은 서버 로그에만 기록.
|
||||
*/
|
||||
@ExceptionHandler({DataAccessException.class, SQLException.class})
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public ApiResponse<Void> handleDataAccess(Exception e) {
|
||||
log.error("데이터 접근 오류", e);
|
||||
return ApiResponse.fail("ERR-MALL-DB: 요청 처리 중 오류가 발생했습니다");
|
||||
}
|
||||
|
||||
/** 권한 부족(메서드 보안 @PreAuthorize 등) — 403 유지, 내부 메시지 미노출. */
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||
public ApiResponse<Void> handleAccessDenied(AccessDeniedException e) {
|
||||
log.warn("권한 부족: {}", e.getMessage());
|
||||
return ApiResponse.fail("ERR-MALL-403: 접근 권한이 없습니다");
|
||||
}
|
||||
|
||||
/** 인증 실패 — 401 유지, 내부 메시지 미노출. */
|
||||
@ExceptionHandler(AuthenticationException.class)
|
||||
@ResponseStatus(HttpStatus.UNAUTHORIZED)
|
||||
public ApiResponse<Void> handleAuthentication(AuthenticationException e) {
|
||||
log.warn("인증 실패: {}", e.getMessage());
|
||||
return ApiResponse.fail("ERR-MALL-401: 인증이 필요합니다");
|
||||
}
|
||||
|
||||
@ExceptionHandler(RuntimeException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public ApiResponse<Void> handleRuntime(RuntimeException e) {
|
||||
// 스택트레이스 미노출 — 에러 코드/요약만 반환
|
||||
// 사전 정의된 비민감 업무 메시지(ERR-XXX-NNN)만 노출.
|
||||
// 미분류 런타임 예외는 SQL/드라이버/내부 원인이 섞일 수 있어 마스킹하고 원인은 로그에만 기록.
|
||||
if (isSafeBusinessMessage(e.getMessage())) {
|
||||
log.warn("업무 오류: {}", e.getMessage());
|
||||
return ApiResponse.fail(e.getMessage());
|
||||
}
|
||||
log.error("미분류 런타임 오류(메시지 마스킹)", e);
|
||||
return ApiResponse.fail("ERR-MALL-500: 요청 처리 중 오류가 발생했습니다");
|
||||
}
|
||||
|
||||
/** 정적 리소스 미존재 — 500 으로 감싸지 않고 깔끔히 404 반환 (SPA 폴백 이후의 실제 미스). */
|
||||
@ExceptionHandler(NoResourceFoundException.class)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user