Spring

Spring Security에서 BadCredentialsException만 발생할 때

@xftg77g 2023. 6. 27. 16:07

CustomUserDetailService를 구현하다가 유저가 없는 경우에 UsernameNotFoundException을 던지게끔 처리했다. 그런데 계속 BadCredentialsException이 발생하는 것이 아닌가.

 

디버깅을 해보니 DaoAuthenticationProvider에서 UsernameNotFoundException를 핸들링할 때 mitigateAgainstTimingAttack이라는 메서드를 호출하고 있고, 그 후에 BadCredentialsException 으로 예외를 랩핑하는 식으로 처리하고 있었다. mitigateAgainstTimingAttack 메서드는 왜 실행하는 걸까?

DaoAuthenticationProvider.java

...
catch (UsernameNotFoundException ex) {
   mitigateAgainstTimingAttack(authentication);
   throw ex;
}

궁금하여 살펴보니, userNotFoundEncodedPassword 이라는 값을 기본적으로 사용하여 passwordEncoder를 통해 굳이 비밀번호를 검증하는 것이었다. 결과는 당연히 실패일텐데 말이다. (이미 없는 아이디라는 사실을 알기 때문이다.) userNotFoundEncodedPassword 에 주석을 통해 그 이유를 따라가 볼 수 있었는데, security 프로젝트에서 SEC-2056으로 보고된 이슈를 해결하기 위해 적용된 방식이었다. (with 부-채널 공격)

 

사실 Security가 제공하는 passwordEncoder 구현체들은 적응형 단방향 함수로써 고성능 하드웨어를 이용한 공격에 방어하기 위해서 인코딩시 약 1초 정도의 시간이 걸리도록 설정 및 사용하는 것을 권장하고 있다. 이는 의도적으로 고성능의 컴퓨팅 리소스를 이용하여 무차별적인 공격에 방어하기 위함이다. (참고) 그런데 유저를 찾을 수 없는 경우 비밀번호 검증을 하지 않는다면 공격자는 몇 ms 안에 유효하지 않은 아이디라는 결과를 확인할 수 있을 것이다. 즉 공격자가 무차별적인 공격을 통해서 유효한 아이디를 찾아낼 수 있게 된다.

 

따라서 시큐리티는 존재하지 않는 아이디로 로그인 요청이 들어오더라도 보안을 위해서 굳이 비밀번호 검증 작업을 수행하는 것이었다. 참고로 해당 이슈는 Spring Security 3.1.3+, 3.0.8+ 또는 2.0.8+로 업그레이드하여 해결할 수 있다.

 

마지막, 그럼 Exception은 왜 랩핑하는 걸까? 이유는 역시 보안때문이다. DaoAuthenticationProvider를 설정할 때 아래 메서드를 통해 랩핑 여부를 결정할 수 있다.

AbstractUserDetailsAuthenticationProvider.java

...
/**
 * By default the<code>AbstractUserDetailsAuthenticationProvider</code>throws a
 *<code>BadCredentialsException</code>if a username is not found or the password is
 * incorrect. Setting this property to<code>false</code>will cause
 *<code>UsernameNotFoundException</code>s to be thrown instead for the former. Note
 * this is considered less secure than throwing<code>BadCredentialsException</code>
* for both exceptions.
 *@paramhideUserNotFoundExceptionsset to<code>false</code>if you wish
 *<code>UsernameNotFoundException</code>s to be thrown instead of the non-specific
 *<code>BadCredentialsException</code>(defaults to<code>true</code>)
 */
public void setHideUserNotFoundExceptions(boolean hideUserNotFoundExceptions) {
   this.hideUserNotFoundExceptions = hideUserNotFoundExceptions;
}