기존 로그인 메서드에서
를 구분 및 반환하여 사용자 열거 위험을 인지하였습니다.
추가적으로 르그인 시 기존 리프레시 토큰을 삭제하지 않아 재발급 시 정합성/중복 문제가 발생할 수 있음을 발견하였습니다.
이를 해결하고자 검증 로직을 하나로 통합하고 로그인 메서드 호출 시 기존 리프레시 토큰을 삭제 후 재발급 하도록 개선하였습니다.
public JwtResponseDto login(UserLoginRequestDto requestDto) {
User user = userRepository.findByUsername(requestDto.getUsername())
.orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
if (!passwordEncoder.matches(requestDto.getPassword(), user.getPassword())) {
throw new CustomException(ErrorCode.INVALID_PASSWORD);
}
UserDetailsImpl userDetails = UserDetailsImpl.build(user);
String accessToken = jwtUtil.generateAccessToken(userDetails);
String refreshToken = refreshTokenService.createRefreshToken(user).getToken();
return new JwtResponseDto(accessToken, refreshToken);
}
public JwtResponseDto login(UserLoginRequestDto requestDto) {
User user = userRepository.findByUsername(requestDto.getUsername()).orElse(null);
if (user == null || !passwordEncoder.matches(requestDto.getPassword(), user.getPassword())) {
throw new CustomException(ErrorCode.INVALID_CREDENTIALS);
}
UserDetailsImpl userDetails = UserDetailsImpl.build(user);
String accessToken = jwtUtil.generateAccessToken(userDetails);
// 단일 세션 정책: 기존 리프레시 토큰 제거 후 재발급
refreshTokenRepository.deleteByUser(user);
String refreshToken = refreshTokenService.createRefreshToken(user).getToken();
return new JwtResponseDto(accessToken, refreshToken);
}
INVALID_CREDENTIALS(HttpStatus.UNAUTHORIZED, "아이디 또는 비밀번호가 올바르지 않습니다.")
공격자가 시스템의 응답 차이를 이용해
특정 아이디가 존재하는지 여부를 알아낼 수 있는 보안 취약점.
즉, 공격자가 로그인 요청을 반복하면서 “이 아이디가 존재하는지 / 존재하지 않는지”를 시스템 응답으로 유추할 수 있습니다.
코드 흐름
// 아이디 없음
orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
// 비밀번호 불일치
throw new CustomException(ErrorCode.INVALID_PASSWORD);
공격자가 username = "testUser"를 입력하고 로그인 시도
• DB에 testUser가 없으면 → USER_NOT_FOUND 반환
• 즉, 공격자는 “testUser는 존재하지 않는다”를 알 수 있음.
공격자가 존재하는 유저의 아이디를 입력하고 잘못된 비밀번호 입력
• 시스템은 INVALID_PASSWORD 반환
• 즉, 공격자는 “이 아이디는 존재한다”를 확인할 수 있음.
응답 메시지를 다르게 주었기 때문에 공격자가 사용자 계정 존재 여부를 확인할 수 있는 취약점 발생
if (user == null || !passwordEncoder.matches(...)) {
throw new CustomException(ErrorCode.INVALID_CREDENTIALS);
}
응답 시나리오
무조건 같은 INVALID_CREDENTIALS 에러만 발생
공격자 요청 예시
POST /api/users/login
Content-Type: application/json
{ "username": "victimUser", "password": "wrongPass" }
• 응답1: {"code": "USER_NOT_FOUND", "message": "존재하지 않는 유저입니다."}
• 응답2: {"code": "INVALID_PASSWORD", "message": "비밀번호가 올바르지 않습니다."}
공격자는 1번 응답이면 계정 없음, 2번 응답이면 계정 존재라고 확신 가능.
{"code": "INVALID_CREDENTIALS", "message": "아이디 또는 비밀번호가 올바르지 않습니다."}
공격자는 계정 존재 여부를 구분할 수 없음.