1
votes

I created a simple app with Spring Boot and Spring security which contains :

  • A login form
  • An "upload" form (with an associated Controller on backend's side)

Problem : Spring security has built-in default CSRF protection. It works well with common REST calls but it prevents me from uploading a file : I get this error message :

Invalid CSRF Token 'null' was found on the request parameter '_csrf' or header 'X-XSRF-TOKEN'.

If I deactivate CSRF protection, I can successfully upload the file.

I created a SSCCE to illustrate the problem. The steps to reproduce are :

  1. Launch the application (Main class is com.denodev.Application)
  2. Connect to localhost:8080
  3. Authenticate with those credentials :
    • Login : user
    • Password : password
  4. When redirected to the "upload" form, try to upload any file.
  5. In class Application, feel free to activate/deactivate CSRF protection, restart the app and retry.

The relevant part of the code is :

@RestController
@SpringBootApplication
public class Application {

  public static void main(String[] args) {
    SpringApplication.run(Application.class);
  }

  @RequestMapping(value = "/upload-file", method = RequestMethod.POST)
  @ResponseBody
  public String uploadFile(@RequestParam("file") MultipartFile file) {
    return "Successfully received file "+file.getOriginalFilename();
  }

  @Configuration
  @Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
  protected static class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Override
    public void configure(HttpSecurity http) throws Exception {
      http
          .authorizeRequests()
          .antMatchers("/", "/**/*.html", "login").permitAll()
          .anyRequest().authenticated()
          .and()
            .formLogin()
            .successHandler(successHandler())
            .failureHandler(failureHandler())
          .and()
            .exceptionHandling()
            .accessDeniedHandler(accessDeniedHandler())
            .authenticationEntryPoint(authenticationEntryPoint())
          .and()

          //1 : Uncomment to activate csrf protection
          .csrf()
          .csrfTokenRepository(csrfTokenRepository())
          .and()
          .addFilterAfter(csrfHeaderFilter(), CsrfFilter.class)

          //2 : Uncomment to disable csrf protection
          //.csrf().disable()
      ;
    }

    /**
     * Return HTTP 200 on authentication success instead of redirecting to a page.
     */
    private AuthenticationSuccessHandler successHandler() {
      return new AuthenticationSuccessHandler() {
        @Override
        public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
          httpServletResponse.setStatus(HttpServletResponse.SC_OK);
        }
      };
    }

    /**
     * Return HTTP 401 on authentication failure instead of redirecting to a page.
     */
    private AuthenticationFailureHandler failureHandler() {
      return new AuthenticationFailureHandler() {
        @Override
        public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
          httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
          httpServletResponse.getWriter().write(e.getMessage());
        }
      };
    }

    /**
     * Return HTTP 403 on "access denied" instead of redirecting to a page.
     */
    private AccessDeniedHandler accessDeniedHandler() {
      return new AccessDeniedHandler() {
        @Override
        public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
          httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
          httpServletResponse.getWriter().write(e.getMessage());
        }
      };
    }

    private AuthenticationEntryPoint authenticationEntryPoint() {
      return new AuthenticationEntryPoint() {
        @Override
        public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
          httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
          httpServletResponse.getWriter().write(e.getMessage());
        }
      };
    }

What I tried :

The Spring security's documentation about Multipart advices to place MultipartFilter before Spring security. It explains well how to do it with a plain old webapp by editing the web.xml file. This is not applicable to Spring Boot and I cannot figure what is the equivalent syntax.

I tried to expose the MultipartFilter with annotations @Bean and Order with several options but I still struggle with it.

Any ideas?

1
Do you send the uploaded file in an AngularJs HTTP request ?Bilal BBB
@BillBilal In the context of this SSCCE, no, but I tried the same with Angular/AJAX call and get the same problem.Arnaud Denoyelle
In my case, it works. I added a directive to upload the file in the client side, after that I send it in an AngularJs POST request. AngularJs adds the X-XSRF-TOKEN token to every HTTP requestBilal BBB
Your example wouldn't work. You don't send the X-XSRF-TOKEN to the server as you do. This is how the CSRF protection is supposed to protect you.Bilal BBB
Another solution is to add the CSRF token in the form that uploads the file (_csrf which is hidden) but I don't know if you will have to do some additional configurations.Bilal BBB

1 Answers

2
votes

This works for me :

Add a directive to upload a file in the client side :

app.directive('fileModel', function ($parse) {

        return {

            restrict: 'A',

            link: function(scope, element, attrs) {

                var model = $parse(attrs.fileModel);
                var modelSetter = model.assign;

                element.bind('change', function(){

                    scope.$apply(function(){
                        modelSetter(scope, element[0].files[0]);
                    });

                });

            }
    };
})

Upload the file :

<input type="file" file-model="fileToUpload"/>

This is how I upload the file to the server :

var formData = new FormData();

formData.append("file", fileToUpload);

$http({

        method: 'POST',
        url: 'the URL',
        headers: {'Content-Type': undefined},
        data: formData,
        transformRequest: angular.identity

})

.success(function(data, status){

})