CSRF Prevention with Spring Security and AngularJS

不想你离开。 提交于 2019-12-13 03:45:46

问题


I'm using Spring 4.3.12.RELEASE Version, AngularJS 1.4.8. I'm trying to prevent the CSRF Attack on the application.

@Configuration
    @Order(2)
    public static class SecurityConfig extends WebSecurityConfigurerAdapter {

        String[] pathsToRemoveAuthorizaton = {
                "/mobile/**",
                "/logout",
                "/j_spring_security_logout",
                "/login",
        };

        private final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);

        @Override
        public void configure(HttpSecurity http) throws Exception {

            logger.info("http configure");
            http.antMatcher("/**").authorizeRequests().antMatchers(pathsToRemoveAuthorizaton).permitAll()
                    .antMatchers("/**").authenticated()
                    .and().formLogin().loginPage("/login")
                    .usernameParameter("employeeId").passwordParameter("password")
                    .successForwardUrl("/dashboard").defaultSuccessUrl("/dashboard", true)
                    .successHandler(customAuthenticationSuccessHandler()).loginProcessingUrl("/j_spring_security_check")
                    .and().logout().logoutSuccessUrl("/logout").logoutUrl("/j_spring_security_logout")
                    .logoutSuccessHandler(customLogoutSuccessHandler()).permitAll().invalidateHttpSession(true)
                    .deleteCookies("JSESSIONID").and().sessionManagement().sessionFixation().newSession()
                    .maximumSessions(1).maxSessionsPreventsLogin(true).and()
                    .sessionCreationPolicy(SessionCreationPolicy.NEVER).invalidSessionUrl("/logout").and()
                    .exceptionHandling().accessDeniedPage("/logout");

//          http.csrf().csrfTokenRepository(csrfTokenRepository()).and()
//          .addFilterAfter(new StatelessCSRFFilter(), CsrfFilter.class);

            http.csrf().csrfTokenRepository(csrfTokenRepository());

//          http.csrf().disable();
//          http.csrf().ignoringAntMatchers("/mobile/**");

            http.authorizeRequests().anyRequest().authenticated();
        }

        private CsrfTokenRepository csrfTokenRepository() {
            HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
            repository.setHeaderName("X-XSRF-TOKEN");
            return repository;
        }


        @Bean
        public AuthenticationSuccessHandler customAuthenticationSuccessHandler() {
            return new CustomAuthenticationSuccessHandler();
        }

        @Bean
        public LogoutSuccessHandler customLogoutSuccessHandler() {
            return new CustomLogoutSuccessHandler();
        }
    }

Below is my angular service code

govtPMS.service('Interceptor', function($q, $location, $rootScope, pinesNotifications, Auth) {

  return {

    request: function(config) {
        config.headers.Authorization = 'Bearer '+$rootScope.authToken;
//        document.cookie = 'CSRF-TOKEN=' + $rootScope.generateKey;

       return config;
    },
    requestError: function (rejection) {
        return $q.reject(rejection);
    },
    response: function(res) {

        if(res.status === 200 || res.status === 201){
            if(res.data.response !== undefined){
                if(res.data.status === 1 || res.data.status === 3 || res.data.status === 2) {
                    pinesNotifications.notify({
                        'title': 'Success',
                        'text': res.data.message,
                        'type': 'success',
                        'delay': 5000
                    });
                }
                 else if(res.data.status === 5) {
                    pinesNotifications.notify({
                        'title': 'Warning',
                        'text': res.data.message,
                        'type': 'warning',
                        'delay': 5000
                    });
                }
            }
        }
      return res || $q.when(res);
    },
    responseError: function(error) {
      return $q.reject(error);
    }
  };
}).config(['$httpProvider', function($httpProvider) {
    $httpProvider.defaults.xsrfHeaderName = 'X-CSRF-TOKEN';
  $httpProvider.defaults.xsrfCookieName = 'CSRF-TOKEN';

  $httpProvider.interceptors.push('Interceptor');

}])

I'm still not able to see the CSRF Token header along with requests.

In this application, we are using 3 jsp pages - login.jsp, logout.jsp and dashboard.jsp angular scope is defined in the dashboard.jsp, hence login and logout are out of scope of AngularJS. I've also tried it the stateless way from this and this examples, where angular is generating a UUID and appending with cookie and request header, the below filter was doing the job fine. Until the logout attack. In this attack, the attacker is trying to succesfully logout the user since to logout from the application, we are simply using a href.

<li><a href="j_spring_security_logout" ><i class="fa fa-sign-out"></i><span>Logout</span></a></li>

Now since its logout is out of angular, the angularjs interceptor is cannot attach the UUID there. I've been struggling on this since past week, Any help will be appreciated.

StatelessCSRFFilter.java

package com.leadwinner.sms.config.filters;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.web.filter.OncePerRequestFilter;

public class StatelessCSRFFilter extends OncePerRequestFilter {



    private static final String CSRF_TOKEN = "CSRF-TOKEN";
    private static final String X_CSRF_TOKEN = "X-CSRF-TOKEN";
    private final AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        List<String> excludedUrls = new ArrayList<>();
        excludedUrls.add("/resources");
        excludedUrls.add("/j_spring_security_check");
        excludedUrls.add("/j_spring_security_logout");
        excludedUrls.add("/login");
        excludedUrls.add("/logout");
        excludedUrls.add("/mobile");
        excludedUrls.add("/migrate");
        excludedUrls.add("/dashboard");

        String path = request.getServletPath();
        System.out.println(path);

        AtomicBoolean ignoreUrl = new AtomicBoolean(false);

        excludedUrls.forEach(url -> {
            if (request.getServletPath().startsWith(url.toLowerCase()) || request.getServletPath().equals("/")) {
                ignoreUrl.set(true);
            }
        });

        if (!ignoreUrl.get()) {
            final String csrfTokenValue = request.getHeader(X_CSRF_TOKEN);
            final Cookie[] cookies = request.getCookies();
            System.out.println("**************************************************");
            System.out.println("--------------------------------------------------");
            String csrfCookieValue = null;
            if (cookies != null) {
                for (Cookie cookie : cookies) {
                    if (cookie.getName().equals(CSRF_TOKEN)) {
                        csrfCookieValue = cookie.getValue();
                    }
                }
            }
            System.out.println("csrfTokenValue = "+csrfTokenValue);
            System.out.println("csrfCookieValue = "+csrfCookieValue);

            System.out.println("--------------------------------------------------");
            System.out.println("**************************************************");
            if (csrfTokenValue == null || !csrfTokenValue.equals(csrfCookieValue)) {
                accessDeniedHandler.handle(request, response, new AccessDeniedException(
                        "Missing or non-matching CSRF-token"));
                return;
            }
        }
        filterChain.doFilter(request, response);
    }
}


回答1:


If the request can be made by the browser and credentials be handed up automatically (session cookie, basic auth creds), then CSRF protection is necessary, even with a mobile API.

Given that you have a mobile API as part of the application, the question is can those APIs be successfully addressed by the browser?

What I'd recommend is that you create two separate filter chains, like so, one for the web app and one for the mobile API:

@Configuration
@Order(100)
public class WebAppConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) {
        http
            .requestMatchers()
                .antMatchers("/app/**")
                .and()
            .authorizeRequests()
                // ...
                .anyRequest().authenticated()
                .and()
            .formLogin();
    }
}

@Configuration
@Order(101)
public class MobileApiConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) {
        http
            .requestMatchers()
                .antMatchers("/api/**")
                .and()
            .authorizeRequests()
                // ...
                .anyRequest().authenticated()
                .and()
            .oauth2ResourceServer()
                .jwt();
    }
}

What this achieves is it separates the two configurations. The endpoints relative to the web application use one setup, and the endpoints relative to the mobile API use another setup.

Now, a couple of comments about the mobile API. I'm assuming that you are authenticating using OAuth 2.0 Bearer tokens, which is why the configuration above uses oauth2ResourceServer() from Spring Security 5.1+. What this does is selectively disables CSRF for requests that contain an Authorization: Bearer xyz header.

But, since you are using Spring Security 4.3, then you may need to do something more like the following (unless you can upgrade):

@Order(101)
public class MobileApiConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) {
        http
            .requestMatchers()
                .antMatchers("/api/**")
                .and()
            .authorizeRequests()
                // ...
                .anyRequest().authenticated()
                .and()
            .sessionManagement().sessionCreationPolicy(NEVER)
                .and()
            .addFilterBefore(new MyMobileAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
            .csrf().disable();
    }
}

What you'd need to make sure of, though, is that your custom authentication filter doesn't use an authentication mechanism that is automatically sent by the browser from any origin (session cookies, Authorization: Basic).




回答2:


`Hi Shiva,

Your code in the configure method of SecurityConfig should look like the below code:

 http
     .authorizeRequests()
     .antMatchers(patterns)
     .permitAll()
       .antMatchers("/hello/**")
       .hasRole("USER")
       .and()
       .csrf()
       .csrfTokenRepository(csrfTokenRepository())
       .requireCsrfProtectionMatcher(csrfProtectionMatcher(patterns))
       .and()
       .httpBasic()
       .and()
       .addFilterAfter(csrfFilter(patterns), FilterSecurityInterceptor.class)
       .addFilterAfter(new StatelessCSRFFilter(), CsrfFilter.class);

And in the StatelessCSRFFilter, use the following code:

 @Override
 public void init(FilterConfig filterConfig) throws ServletException {}

 @Override
 public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
     CsrfToken csrf = (CsrfToken) servletRequest.getAttribute(CsrfToken.class.getName());
     String token = csrf.getToken();
     if (token != null && isAuthenticating(servletRequest)) {
         HttpServletResponse response = (HttpServletResponse) servletResponse;
         Cookie cookie = new Cookie("XSRF-TOKEN", token);
         cookie.setPath("/");
         response.addCookie(cookie);
     }
     filterChain.doFilter(servletRequest, servletResponse);
 }

 private boolean isAuthenticating(ServletRequest servletRequest) {
     HttpServletRequest request = (HttpServletRequest) servletRequest;
     return request.getRequestURI().equals("/login");
 }`



回答3:


<pre>
	Add this code for the csrfTokenRepository method
	     private CsrfTokenRepository csrfTokenRepository() {
	         HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
	         repository.setHeaderName("X-XSRF-TOKEN");
	         return repository;
	     }
	Add this for the csrfFilter method
	     private Filter csrfFilter(String[] patterns) {
	         CsrfFilter csrfFilter = new CsrfFilter(csrfTokenRepository());
	         csrfFilter.setRequireCsrfProtectionMatcher(csrfProtectionMatcher(patterns));
	         return csrfFilter;
	     }
	Add this for the csrfProtectionMatcher method
	     private NoAntPathRequestMatcher csrfProtectionMatcher(String[] patterns) {
	         return new NoAntPathRequestMatcher(patterns);
	     }
	Also remove these lines in configure method
	       .csrfTokenRepository(csrfTokenRepository())
	       .requireCsrfProtectionMatcher(csrfProtectionMatcher(patterns))
	Move these lines below .csrf() in configure method:
	       .and()
	       .addFilterAfter(csrfFilter(patterns), FilterSecurityInterceptor.class)
</pre>


来源:https://stackoverflow.com/questions/58911887/csrf-prevention-with-spring-security-and-angularjs

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!