사용자 열거(User enumeration)는 웹 애플리케이션 데이터베이스에 존재하는 사용자의 이름을 추측하는 기능을 말합니다. 이것은 웹사이트의 “로그인 / 로그아웃 / 비밀번호 찾기” 등을 수행할 때 발생할 수 있습니다.
유저가 잘못된 이름으로 웹사이트에 “로그인”을 시도할 때, 웹 애플리케이션은 “올바른 사용자의 이름이 아닙니다.”와 유사한 메시지와 함께 사용자의 이름이 존재하지 않는다는 사실을 드러내서는 안됩니다. 대신, “잘못된 자격증명입니다.” 처럼 인증 과정에서 사용자의 이름이나 암호가 잘못되었는지를 추측할 수 없는 일반적인 메시지를 표시해야 합니다.
만약 유저 관리 기능이 사용자 이름이 존재하는지에 대한 정보를 공개하는 경우, 공격자는 무차별 대입 공격을 통해 피싱, 패스워드 추측 등을 용이하게 할 수 있는 많은 양의 유효한 사용자 이름을 검색할 수 있습니다.
다음과 같은지 자문해보세요.
- 애플리케이션이 데이터베이스에 사용자 이름이 존재하지는지를 드러냅니다: 웹 사이트의 다른 사용자가 사용중이지 않은 유효한 유저이름을 선택해야 하는 “회원가입 / 로그인” 부분 등을 제외하고는, 대부분의 경우 이러한 종류의 유출을 피할 수 있습니다.
- 사용자 이름과 관련된 요청에 속도 제한이나 CAPTCHA 보호가 적용되어있지 않습니다.
추천되는 안전한 코딩 관습
사용자가 사용자 이름과 관련된 요청을 수행할 때 유효한 사용자 이름과 잘못된 사용자 이름 간의 차이점을 발견하는 것이 불가능해야 합니다.
- 오류 메시지는 일반적이어야 하며 사용자 이름이 유효한지 여부를 공개하지 않아야 합니다.
- 응답 시간은 유효한 사용자 이름과 유사해야 합니다.
- CAPTCHA 및 기타 속도 제한 솔루션을 구현해야 합니다.
민감한 코드의 예
스프링 시큐리티 웹 애플리케이션에서 다음과 같은 경우 사용자 이름이 누출됩니다.:
- loadUserByUsername 메서드의 인수로 사용되는 문자열이 예외 메시지에서 사용되는 경우.
public String authenticate(String username, String password) {
// ....
MyUserDetailsService s1 = new MyUserDetailsService();
MyUserPrincipal u1 = s1.loadUserByUsername(username);
if(u1 == null) {
throw new BadCredentialsException(username+" doesn't exist in our database"); // 민감한 코드
}
// ....
}
-
UsernameNotFoundException이 발생한 경우(
loadUserByUsername
메서드에 있는 경우 제외).
public String authenticate(String username, String password) {
// ....
if(user == null) {
throw new UsernameNotFoundException("user not found"); // 민감한 코드
}
// ....
}
-
HideUserNotFoundExceptions가
false
로 설정된 경우.
DaoAuthenticationProvider daoauth = new DaoAuthenticationProvider();
daoauth.setUserDetailsService(new MyUserDetailsService());
daoauth.setPasswordEncoder(new BCryptPasswordEncoder());
daoauth.setHideUserNotFoundExceptions(false); // 민감한 코드
builder.authenticationProvider(daoauth);
규칙을 준수한 해결책
스프링 시큐리티 웹 애플리케이션에서는
- 잘못된 사용자인지 암호인지에 관계없이 동일한 메시지를 사용해야 합니다:
public String authenticate(String username, String password) throws AuthenticationException {
Details user = null;
try {
user = loadUserByUsername(username);
} catch (UsernameNotFoundException | DataAccessException e) {
// 유저이름이 존재하지 않음을 드러내지 않으려면 이 exception 의 이유를 숨겨야합니다.
}
if (user == null || !user.isPasswordCorrect(password)) {
// 유저는 "Bad credential" 메시지가 유저 이름과 관련있는지 혹은 비밀번호 때문인지를 추측할 수 없어야 합니다.
throw new BadCredentialsException("Bad credentials");
}
}
- HideUserNotFoundExceptions 는 true 로 설정되어야 합니다.:
DaoAuthenticationProvider daoauth = new DaoAuthenticationProvider();
daoauth.setUserDetailsService(new MyUserDetailsService());
daoauth.setPasswordEncoder(new BCryptPasswordEncoder());
daoauth.setHideUserNotFoundExceptions(true); // 규칙을 준수한 코드
builder.authenticationProvider(daoauth);
참고
- OWASP Top 10 2021 Category A1 - Broken Access Control
- OWASP Top 10 2017 Category A2 - Broken Authentication
- MITRE, CWE-200 - Exposure of Sensitive Information to an Unauthorized Actor
If you like SONARKUBE, don’t forget to give me a star.