Jelajahi Sumber

SEC-2056: DaoAuthenticationProvider performs isPasswordValid when user not found

Previously authenticating a user could take significantly longer than
determining that a user does not exist. This was due to the fact that only
users that were found would use the password encoder and comparing a
password can take a significant amount of time. The difference in the
time required could allow a side channel attack that reveals if a user
exists.

The code has been updated to do comparison against a dummy password
even when the the user was not found.
Rob Winch 13 tahun lalu
induk
melakukan
f5fc94e1be

+ 29 - 2
core/src/main/java/org/springframework/security/providers/dao/DaoAuthenticationProvider.java

@@ -24,6 +24,7 @@ import org.springframework.security.providers.encoding.PasswordEncoder;
 import org.springframework.security.providers.encoding.PlaintextPasswordEncoder;
 import org.springframework.security.userdetails.UserDetails;
 import org.springframework.security.userdetails.UserDetailsService;
+import org.springframework.security.userdetails.UsernameNotFoundException;
 import org.springframework.dao.DataAccessException;
 import org.springframework.util.Assert;
 
@@ -35,10 +36,24 @@ import org.springframework.util.Assert;
  * @version $Id$
  */
 public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
+    //~ Static fields/initializers =====================================================================================
+
+    /**
+     * The plaintext password used to perform {@link PasswordEncoder#isPasswordValid(String, String, Object)} on when the user is
+     * not found to avoid SEC-2056.
+     */
+    private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
 
     //~ Instance fields ================================================================================================
 
-    private PasswordEncoder passwordEncoder = new PlaintextPasswordEncoder();
+    private PasswordEncoder passwordEncoder;
+
+    /**
+     * The password used to perform {@link PasswordEncoder#isPasswordValid(String, String, Object)} on when the user is
+     * not found to avoid SEC-2056. This is necessary, because some {@link PasswordEncoder} implementations will short circuit if the
+     * password is not in a valid format.
+     */
+    private String userNotFoundEncodedPassword;
 
     private SaltSource saltSource;
 
@@ -46,6 +61,10 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication
 
     private boolean includeDetailsObject = true;
 
+    public DaoAuthenticationProvider() {
+        setPasswordEncoder(new PlaintextPasswordEncoder());
+    }
+
     //~ Methods ========================================================================================================
 
     protected void additionalAuthenticationChecks(UserDetails userDetails,
@@ -85,6 +104,13 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication
         catch (DataAccessException repositoryProblem) {
             throw new AuthenticationServiceException(repositoryProblem.getMessage(), repositoryProblem);
         }
+        catch (UsernameNotFoundException notFound) {
+            if(authentication.getCredentials() != null) {
+                String presentedPassword = authentication.getCredentials().toString();
+                passwordEncoder.isPasswordValid(userNotFoundEncodedPassword, presentedPassword, null);
+            }
+            throw notFound;
+        }
 
         if (loadedUser == null) {
             throw new AuthenticationServiceException(
@@ -100,6 +126,7 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication
      * @param passwordEncoder The passwordEncoder to use
      */
     public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
+        this.userNotFoundEncodedPassword = passwordEncoder.encodePassword(USER_NOT_FOUND_PASSWORD, null);
         this.passwordEncoder = passwordEncoder;
     }
 
@@ -143,6 +170,6 @@ public class DaoAuthenticationProvider extends AbstractUserDetailsAuthentication
      */
     public void setIncludeDetailsObject(boolean includeDetailsObject) {
         this.includeDetailsObject = includeDetailsObject;
-	}
+    }
 
 }

+ 42 - 0
core/src/test/java/org/springframework/security/providers/dao/DaoAuthenticationProviderTests.java

@@ -15,6 +15,13 @@
 
 package org.springframework.security.providers.dao;
 
+import static org.mockito.Matchers.anyObject;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Matchers.isA;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
 import junit.framework.TestCase;
 
 import org.springframework.security.AccountExpiredException;
@@ -32,6 +39,7 @@ import org.springframework.security.providers.UsernamePasswordAuthenticationToke
 import org.springframework.security.providers.dao.cache.EhCacheBasedUserCache;
 import org.springframework.security.providers.dao.cache.NullUserCache;
 import org.springframework.security.providers.dao.salt.SystemWideSaltSource;
+import org.springframework.security.providers.encoding.PasswordEncoder;
 import org.springframework.security.providers.encoding.ShaPasswordEncoder;
 
 import org.springframework.security.userdetails.User;
@@ -436,6 +444,40 @@ public class DaoAuthenticationProviderTests extends TestCase {
         assertTrue(!provider.supports(TestingAuthenticationToken.class));
     }
 
+
+    // SEC-2056
+    public void testUserNotFoundEncodesPassword() {
+        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("missing", "koala");
+        PasswordEncoder encoder = mock(PasswordEncoder.class);
+        when(encoder.encodePassword(anyString(), anyObject())).thenReturn("koala");
+        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
+        provider.setHideUserNotFoundExceptions(false);
+        provider.setPasswordEncoder(encoder);
+        provider.setUserDetailsService(new MockAuthenticationDaoUserrod());
+        try {
+            provider.authenticate(token);
+            fail("Expected Exception");
+        } catch(UsernameNotFoundException success) {}
+
+        // ensure encoder invoked w/ non-null strings since PasswordEncoder impls may fail if encoded password is null
+        verify(encoder).isPasswordValid(isA(String.class),  isA(String.class), anyObject());
+    }
+
+    public void testUserNotFoundNullCredentials() {
+        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken("missing", null);
+        PasswordEncoder encoder = mock(PasswordEncoder.class);
+        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
+        provider.setHideUserNotFoundExceptions(false);
+        provider.setPasswordEncoder(encoder);
+        provider.setUserDetailsService(new MockAuthenticationDaoUserrod());
+        try {
+            provider.authenticate(token);
+            fail("Expected Exception");
+        } catch(UsernameNotFoundException success) {}
+
+        verify(encoder,times(0)).isPasswordValid(anyString(), anyString(), anyObject());
+    }
+
     //~ Inner Classes ==================================================================================================
 
     private class MockAuthenticationDaoReturnsNull implements UserDetailsService {