Effects of bad filter order in Spring Security

Spring Security is based on filter chain. Every filter can be invoked for appropriate request and executed to provide supplementary layer of protection. But one important thing must be respected - filters order.

In this article we'll try to misconfigure Spring Security filters and prove what can happen in the cases of bad configuration. To do that we'll begin by configuring filters manually, through a bean representing security filter. After describing this part we'll talk about several dangerous situations which can occur when filter chain is misconfigured.

Configure Spring Security filter chain manually

First of all, we need to configure Spring Security filters in correct order. As specified in the documentation of this project, the correct order should be:

  1. ChannelProcessingFilter
  2. SecurityContextPersistenceFilter
  3. ConcurrentSessionFilter
  4. authentication filters, UsernamePasswordAuthenticationFilter in our case
  5. SecurityContextHolderAwareRequestFilter
  6. RememberMeAuthenticationFilter
  7. AnonymousAuthenticationFilter
  8. ExceptionTranslationFilter
  9. FilterSecurityInterceptor

First of all, we'll define the beans for every used filter. Final configuration looks like:

show valid Spring Security filter chain bean configuration

Now we should check if given configuration is really valid. So, let's start Tomcat with HTTPS protocol activated. By going to http://localhost:8080/login we should be redirected to https://localhost:8080/login. And after that, we should be able to login with one of given credentials. If it's the case, we should check if we're able to access to /secred/data URI only being an user having ROLE_ADMIN (admin or bartosz). If it's still the case, we should test logout too (/logout).

If everything works well, we can start to mix filters order.

Bad filter order in Spring Security

Below list contains some testes cases of filters mix.

  1. Filters order: channelProcessingFilter,concurrentSessionFilter, logoutFilter, authenticationFilter, anonymousAuthenticationFilter, securityContextPersistentFilter, filterSecurityInterceptor

    What happens ?

    Comparing to correct configuration, security context persistent filter is placed directly before security interceptor. Usually SecurityContextPersistenceFilter loads security context from security context repository (HTTP session for example). Usually there're no security context in the repository and a new one is created. We can observe that in the logs with following entries:

    DEBUG: org.springframework.security.web.context.HttpSessionSecurityContextRepository - No HttpSession currently exists
    DEBUG: org.springframework.security.web.context.HttpSessionSecurityContextRepository - No SecurityContext was available from the HttpSession: null. A new one will be created.
    

    This new context is after used by the rest of filters. And the end it's saved in security context repository. Saved context contains all changes made by applied filters. So, if user logged successfully, final security context will contain valid Authentication object. And this Authentication object will be saved in repository. Thanks to this save security context can persist between the requests.

    In our misconfigured case, SecurityContextPersistenceFilter tries to persist new and empty security context. Even that, before executing it, others filters make theirs jobs. To check what can happen, you can go to login page (/login). Normally you should see following error message on your screen:

    HTTP 500 - Request processing failed; nested exception is org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContext
    
  2. Filters order: channelProcessingFilter,securityContextPersistentFilter, filterSecurityInterceptor, concurrentSessionFilter, logoutFilter, authenticationFilter, anonymousAuthenticationFilter

    What happens ?

    As you can deduce, we'll receive similar error message to previously discovered, when filterSecurityInterceptor will be placed directly after SecurityContextPersistentFilter. In this situation, when we try to access protected page (for example: /secret/data) as not connected user, we should receive following message:

    HTTP 500 - An Authentication object was not found in the SecurityContext
    

    Remember that anonymousAuthenticationFilter is important because it will put an Authentication object into security context when this object is missing here :

    if (applyAnonymousForThisRequest((HttpServletRequest) req)) {
      if (SecurityContextHolder.getContext().getAuthentication() == null) {
        SecurityContextHolder.getContext().setAuthentication(createAuthentication((HttpServletRequest) req));
      }
    }
    
    protected Authentication createAuthentication(HttpServletRequest request) {
      AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key, principal, authorities);
      auth.setDetails(authenticationDetailsSource.buildDetails(request));
    
      return auth;
    }
    

    Error message is thrown by one of implementations of filter interceptor, org.springframework.security.access.intercept.AbstractSecurityInterceptor, in beforeInvocation method:

    if (SecurityContextHolder.getContext().getAuthentication() == null) {
      credentialsNotFound(messages.getMessage("AbstractSecurityInterceptor.authenticationNotFound", 
          "An Authentication object was not found in the SecurityContext"), object, attributes);
    }
    
    private void credentialsNotFound(String reason, Object secureObject, Collection configAttribs) {
      AuthenticationCredentialsNotFoundException exception = new AuthenticationCredentialsNotFoundException(reason);
    
      AuthenticationCredentialsNotFoundEvent event = new AuthenticationCredentialsNotFoundEvent(secureObject,
      configAttribs, exception);
      publishEvent(event);
    
      throw exception;
    }
    
  3. Filters order: channelProcessingFilter,concurrentSessionFilter, customBreakerFilter, logoutFilter, authenticationFilter, anonymousAuthenticationFilter, securityContextPersistentFilter, filterSecurityInterceptor

    What happens ?

    Another dangerous situation can be produced by our, developers, mistake. As we mentioned in the article about filter chain in Spring Security, almost all filters invoke doFilter(ServletRequest request, ServletResponse response) method of FilterChain interface. Thanks to that, filters remaining in the filter chain will be invoked too. Without that, filters invokation will be stopped.

    Imagine now that we implement our custom filter and that it doesn't call FilterChain's doFilter method. In this case different situations can occur, depending on filters called before it. In the best case, we can provoke the display of blank page. In the worst, we can authorize user to make some operations that he shouldn't be able to do. This first situation is presented in below custom filter implementation:

    /**
     * This class shows how a bad implemented custom filter can break Spring Security filter chain.
     *
     * @author Bartosz Konieczny
     */
    public class ChainBreakingAuthFilter extends GenericFilterBean {
    
      private static final Logger LOGGER = LoggerFactory.getLogger(ChainBreakingAuthFilter.class);
      private AuthenticationManager authenticationManager;
      private UserDetailsService userDetailsService;
    
      public void setAuthenticationManager(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
      }
    
      public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
      }
    
      @Override
      public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
          throws IOException, ServletException {
          if (SecurityContextHolder.getContext().getAuthentication() == null || 
              !SecurityContextHolder.getContext().getAuthentication().isAuthenticated()) {  
            UserDetails user = userDetailsService.loadUserByUsername("normal");
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(user.getUsername(),
                user.getPassword());
            Authentication authentication = this.authenticationManager.authenticate(authRequest);
            SecurityContextHolder.getContext().setAuthentication(authentication);
            ((HttpServletResponse) response).sendRedirect("/secret/data");
        }
      }
    }
    

    Such constructed filter will put new Authentication object into security context and stops filter chain execution. It will lead into the situation when user will see blank page.

    This pitfail can seem a little bit too fictif and should be detectable easily. However in some situations, for example when this filter is applied only to one from multiple routes, the error risk to pass almost unnoticed. Especially as given filter worked well before.

  4. Filters order: channelProcessingFilter, concurrentSessionFilter, logoutFilter, authenticationFilter, anonymousAuthenticationFilter, filterSecurityInterceptor

    What happens ?

    In this chain you can note that SecurityContextPersistenceFilter was removed. It means that Authentication object, even valid, won't be persist through different requests. When you login and try to access protected resource, you'll notice that your security context contains anonymous user and not authenticated one:

    05-09-2014 19:19:54;445 : DEBUG: org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter - Request is to process 
    authentication
    05-09-2014 19:19:54;447 : DEBUG: org.springframework.security.authentication.ProviderManager - Authentication attempt using org.springframework.security.authentication.dao.DaoAuthenticationProvider
    05-09-2014 19:19:54;449 : DEBUG: org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter - 
    Authentication success. Updating SecurityContextHolder to contain: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@1766c55b: Principal: 
    org.springframework.security.core.userdetails.User@ec25a4b5: Username: bartosz; Password: [PROTECTED]; Enabled: 
    true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_ADMIN,ROLE_USER; Credentials: [PROTECTED]; Authenticated: 
    true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@255f8: RemoteIpAddress: 127.0.0.1; 
    SessionId: EF20CFF8F364E5E878E274884ABFD7C9; Granted Authorities: ROLE_ADMIN, ROLE_USER
    DEBUG: org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler - Using default Url: /
    // ... some lines further
    DEBUG: org.springframework.security.web.util.matcher.AntPathRequestMatcher - Checking match of request : '/secret/data'; 
    against '/secret/*'
    DEBUG: org.springframework.security.web.authentication.AnonymousAuthenticationFilter - Populated SecurityContextHolder with anonymous token: 
    'org.springframework.security.authentication.AnonymousAuthenticationToken@90572420: Principal: anonymousUser; 
    Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@255f8: RemoteIpAddress: 127.0.0.1; 
    SessionId: EF20CFF8F364E5E878E274884ABFD7C9; Granted Authorities: ROLE_ANONYMOUS'
    DEBUG: org.springframework.security.web.FilterChainProxy - /secret/data at position 6 of 6 in additional 
    filter chain; firing Filter: 'FilterSecurityInterceptor'
    DEBUG: org.springframework.security.web.util.matcher.AntPathRequestMatcher - Checking match of request : 
    '/secret/data'; against '/secret/**'
    DEBUG: org.springframework.security.web.access.intercept.FilterSecurityInterceptor - Secure object: FilterInvocation:
     URL: /secret/data; Attributes: [ROLE_ADMIN]
    DEBUG: org.springframework.security.web.access.intercept.FilterSecurityInterceptor - Previously Authenticated: 
    org.springframework.security.authentication.AnonymousAuthenticationToken@90572420: Principal: anonymousUser; 
    Credentials: [PROTECTED]; Authenticated: true; Details: 
    org.springframework.security.web.authentication.WebAuthenticationDetails@255f8: RemoteIpAddress: 127.0.0.1;
     SessionId: EF20CFF8F364E5E878E274884ABFD7C9; Granted Authorities: ROLE_ANONYMOUS
    DEBUG: org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl - getReachableGrantedAuthorities() - 
    From the roles [ROLE_ANONYMOUS] one can reach [ROLE_ANONYMOUS] in zero or more steps.
    
  5. Filters order: channelProcessingFilter,securityContextPersistentFilter, logoutFilter, authenticationFilter, anonymousAuthenticationFilter, filterSecurityInterceptor

    What happens ?

    This time ConcurrentSessionFilter is not defined. As we could read in the article about session management in Spring Security, this filter is used mainly to handle session validity. If one session is invalid (expired, hacked), ConcurrentSessionFilter is employed to invalidate it in the session registry. If the session represents logged user, the filter also logouts the user. For example, without this protection, session fixation attack will be possible.

    To see how desthis attack work, just follow given steps:

    1. Remove ConcurrentSessionFilter from your Spring Security filter chain
    2. Redeploy
    3. Open the first browser (represents the aggressor), go to the login page and try to login with bad credentials (empty login and password).
    4. Pick up id of your session (for example: included in a cookie, as JSESSIONID=428471B8EE4EA192E895237B1F0035BE).
    5. Open the second browser (represents the victim), go to the login page and change JSESSION cookie (for example with Chromium's Edit this cookie extension). Replace current value with the value picked up previously (428471B8EE4EA192E895237B1F0035BE).
    6. Refresh the page and login with correct credentials.
    7. Check if protected page is accessible (for example: /secret/data page).
    8. If it is, open the aggressor's browser and go to protected page (/secret/data). Aggressor, even if he didn't do any successful authentication, should be able to access protected content, exactly as the victim. Both use the same session id and without ConcurrentSessionFilter we can't control the maximum number of live sessions belonging to one user.

In this article we discovered the dangers which can be produced when we bad configure Spring Security filter chain. In the first part we saw how to configure Spring Security with explicit beans and not Spring Security namespace elements. After that we listed some examples of bad configuration and effects produced by this configuration. Mainly, we saw the problems with security context consistency. But the last example, illustrating the absence of session concurrent filter, proved that bad configured Spring Security can provoke serious security leaks, as session fixation problem.

If you liked it, you should read:

The comments are moderated. I publish them when I answer, so don't worry if you don't see yours immediately :)

📚 Newsletter Get new posts, recommended reading and other exclusive information every week. SPAM free - no 3rd party ads, only the information about waitingforcode!