원본 본문으로 이동하기

Spring boot Security : 3. 인증로직 - 잠재적 위험

박용서 - Spring boot Security 시리즈 1. 설치 및 페이지 설정 - https://gs.saro.me/#!m=elec&jn=790 2. 인증로직을 만들어보자. - https://gs.saro.me/#!m=elec&jn=791 3. 인증로직 - 잠재적 위험 - https://gs.saro.me/#!m=elec&jn=792 4. 인증 페이지뷰 - https://gs.saro.me/#!m=elec&jn=793 5. 회원가입 - https://gs.saro.me/#!m=elec&jn=794 부록 : Spring Security login (성공 / 실패) 이벤트 리스너 - https://gs.saro.me/#!m=elec&jn=825 서론 이전장인 2. 인증로직을 만들어보자. 를 하는 도중 돌아가는것을 보기위해서 print 를 찍어보았습니다. 무언가를 잠깐 확인 하고 싶었던 것 이라 logger 가아닌 sysout 을 찍었습니다... public static class MyPasswordEncoder implements PasswordEncoder { @Override public String encode(CharSequence rawPassword) { System.out.println("MyPasswordEncoder.encode : " + rawPassword); return "EN-" + rawPassword.toString(); } @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { System.out.println("MyPasswordEncoder.matches.rawPassword : [" + rawPassword + "]"); System.out.println("MyPasswordEncoder.matchesencodedPassword : [" + encodedPassword + "]"); return encodedPassword.equals(encode(rawPassword)); } } 그리고 존재하지 않는 계정을 써봤습니다. 예를들어 not / 1234 로 입력해봤습니다. 분명 아래의 로직에서 걸려서 throw 되어야하는 경우로 보여지는데... @Override public UserDetails loadUserByUsername(String ac) throws UsernameNotFoundException { Account account = accountService.getAccount(ac); if (account == null) { // 계정이 존재하지 않음 throw new UsernameNotFoundException("login fail"); } return new LoginUserDetails(account); } 통과하고 오는겁니다. MyPasswordEncoder.matches.rawPassword : [1234] MyPasswordEncoder.matchesencodedPassword : [EN-userNotFoundPassword] MyPasswordEncoder.encode : 1234 그래서 혹시나하고 계정이 존재하나? 라는 생각에 프린트를 하나더 찍어봤습니다. @Override public UserDetails loadUserByUsername(String ac) throws UsernameNotFoundException { Account account = accountService.getAccount(ac); if (account == null) { // 계정이 존재하지 않음 System.out.println("loadUserByUsername : not existed user"); throw new UsernameNotFoundException("login fail"); } return new LoginUserDetails(account); } 결과 loadUserByUsername : not existed user MyPasswordEncoder.matches.rawPassword : [1234] MyPasswordEncoder.matchesencodedPassword : [EN-userNotFoundPassword] MyPasswordEncoder.encode : 1234 이게뭐야........ 무서워;;;; 문제 유저가 존재하지 않지만 (loadUserByUsername : not existed user) 계속 로직이 진행되어 MyPasswordEncoder 를 호출합니다.... 심지어 userNotFoundPassword 라는 기본값을 인코딩해서 말이죠.!!! (스프링 앱이 실행될때 처음 userNotFoundPassword 값을 스스로 인코딩합니다.) 그래서 테스트를 한번 해보았습니다. @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { System.out.println("MyPasswordEncoder.matches.rawPassword : [" + rawPassword + "]"); System.out.println("MyPasswordEncoder.matchesencodedPassword : [" + encodedPassword + "]"); //return encodedPassword.equals(encode(rawPassword)); return true; } userNotFoundPassword 를 암호로 입력해도되지만, 더 확실히 보기위해 무조건 통과하도록 코드를 짰습니다. 그리고 다시 존재하지 않는 계정으로 입력하였습니다. not / 1234 loadUserByUsername : not existed user MyPasswordEncoder.matches.rawPassword : [1234] MyPasswordEncoder.matchesencodedPassword : [EN-userNotFoundPassword] 다행히 로그는 이렇게 뜨더라도 통과시켜주진 않습니다.!! 존재하는 계정인 test 와 무작위 암호를 입력했을때는 통과하는 걸로봐서 MyPasswordEncoder.matches 이후에 해당 계정이 존재하는지 다시 확인하는 것 같습니다. 물론 스프링에서 제공해주는 BCryptPasswordEncoder 는 어떤지 디버그레벨로 로그도 찍어보고 디컴파일러로 소스도 봤지만 결과는 같습니다. 결론 결론부터 말씀드리면 지금은 userNotFoundPassword 를 예외 처리하지 않더라도 전혀 문제가 없습니다. 하지만 이건 잠재적인 구조 오류 같습니다. 누군가 실수로 스프링 코드에 이부분을 잘못 커밋한 경우 null 계정으로 로그인을 시도하다 다른 오류들을 끌어 들일 수 있다고 생각합니다. 추신 제가 잠시(?) 보안과였다가 프로그래밍이 더 나은 것 같아 몇 달만에 전과한 관계로 BCryptPasswordEncoder 의 솔트가 궁금해졌습니다. 역시 예상대로 인것 같습니다. 솔트를 섞어 해시한뒤 솔트값을 어떤형태로든 끼워넣는 것 같습니다. BCryptPasswordEncoder en = new BCryptPasswordEncoder(); en.matches("userNotFoundPassword", "$2a$10$FZAX65mJfIVTALxAUU4YH.mWU35.cIU8kXIxvwO2anYQ0yGOirRJe"); // == true en.matches("userNotFoundPassword", "$2a$10$2cYJbhEMcZaAqlkPGhyk6eTjFmhDNiinFn8csGIOe6UNJrfJI/TJa"); // == true 즉, 보안적으로 솔트서버를 별도로 두고 매번 값을 가져와서 합쳐 확인하는 극단적인 형태가 아닌 솔트를 같이 첨부하는 형태인 것 같습니다. 사실 이정도만 해도 레인보우 테이블은 어느정도 피할 수 있습니다. (완성된 테이블에서 해시로 빼오는것이 아닌 패스워드표(레인보우테이블)을 보고 해킹한 DB에서 각각 솔트를 뽑아내서 다시 인코딩해서 공객해야함으로 암호를 뽑아내는데 훨씬 많은 시간자원이 듭니다.) 추신2 : 로그 유저 : not (존재하지 않는 유저) 암호 : userNotFoundPassword MyPasswordEncoder 로 실행한 경우 loadUserByUsername : not existed user MyPasswordEncoder.matches.rawPassword : [userNotFoundPassword] MyPasswordEncoder.matchesencodedPassword : [EN-userNotFoundPassword] 2016-07-25 05:29:34 DEBUG o.s.s.a.d.DaoAuthenticationProvider - User 'not' not found 2016-07-25 05:29:34 DEBUG o.s.s.w.a.UsernamePasswordAuthenticationFilter - Authentication request failed: org.springframework.security.authentication.BadCredentialsException: Bad credentials 2016-07-25 05:29:34 DEBUG o.s.s.w.a.UsernamePasswordAuthenticationFilter - Updated SecurityContextHolder to contain null Authentication 2016-07-25 05:29:34 DEBUG o.s.s.w.a.UsernamePasswordAuthenticationFilter - Delegating to authentication failure handler org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler@557ff8e5 2016-07-25 05:29:34 DEBUG o.s.s.w.a.SimpleUrlAuthenticationFailureHandler - Redirecting to /loginForm?error 2016-07-25 05:29:34 DEBUG o.s.s.web.DefaultRedirectStrategy - Redirecting to '/loginForm?error' BCryptPasswordEncoder로 실행한 경우 loadUserByUsername : not existed user 2016-07-25 06:02:13 DEBUG o.s.s.a.d.DaoAuthenticationProvider - User 'not' not found 2016-07-25 06:02:13 DEBUG o.s.s.w.a.UsernamePasswordAuthenticationFilter - Authentication request failed: org.springframework.security.authentication.BadCredentialsException: Bad credentials 2016-07-25 06:02:13 DEBUG o.s.s.w.a.UsernamePasswordAuthenticationFilter - Updated SecurityContextHolder to contain null Authentication 2016-07-25 06:02:13 DEBUG o.s.s.w.a.UsernamePasswordAuthenticationFilter - Delegating to authentication failure handler org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler@7e19a415 2016-07-25 06:02:13 DEBUG o.s.s.w.a.SimpleUrlAuthenticationFailureHandler - Redirecting to /loginForm?error 2016-07-25 06:02:13 DEBUG o.s.s.web.DefaultRedirectStrategy - Redirecting to '/loginForm?error' - 인증서 스프링 자바