09 Attack Responses How to Implement Csrf Protection and Cross Domain Cors

09 Attack Responses How to Implement CSRF Protection and Cross-Domain CORS #

Now that we have mastered the multiple core functionalities provided by Spring Security, as mentioned in the preface, there are more system security issues we need to consider. Today, we will discuss two common security topics in the daily development process: CSRF and CORS. These two abbreviations may seem unfamiliar, but they are related to every request made by an application, and Spring Security provides good development support for them.

Using Spring Security for CSRF Protection #

Let’s start with CSRF. CSRF stands for Cross-Site Request Forgery. So, what exactly is Cross-Site Request Forgery, and how do we deal with it? Please continue reading.

What is CSRF? #

From a security perspective, CSRF can be understood as an attack where the attacker steals your identity and sends malicious requests to a third-party website on your behalf. We can describe CSRF using the following flowchart:

CSRF Flowchart

The specific flow is as follows:

  • The user browses and logs into a trusted website A, and after user authentication, a cookie specifically for website A is generated in the browser.
  • The user, without logging out of website A, visits website B, and then website B sends a request to website A.
  • The user’s browser, based on the request from website B, sends the request to website A with the cookie included.
  • Since the browser automatically includes the user’s cookie, website A receives the request and performs access control based on the user’s permissions. This means that website B is able to simulate the process of the user accessing website A.

Obviously, from the perspective of application development, CSRF is a security vulnerability in the system, and this kind of security vulnerability is also widely present in web development.

Based on the workflow of CSRF, the basic idea of implementing CSRF protection is to add a random value to every link request in the system, which we call the csrf_token. This way, when a user sends a request to website A, website A sets a csrf_token value in the generated cookie. When the browser sends the request, it includes a hidden csrf_token value in the submitted form data. After receiving the request, website A compares the csrf_token extracted from the cookie with the one obtained from the form data. If they do not match, it means that this is a forged request.

Using CsrfFilter #

In Spring Security, there is a CsrfFilter specifically designed to protect against CSRF. The CsrfFilter intercepts requests and allows requests using HTTP methods such as GET, HEAD, TRACE, and OPTIONS. For other requests that may modify data, such as PUT, POST, DELETE, the CsrfFilter expects to receive a message header that contains the csrf_token. If this message header does not exist or contains an incorrect csrf_token value, the application will reject the request and set the response status to 403.

At this point, you may wonder what the csrf_token actually looks like. In essence, it is a string. In Spring Security, a CsrfToken interface is specifically defined to specify its format:

public interface CsrfToken extends Serializable {

    // Get the name of the message header

    String getHeaderName();

    // Get the parameter name that should include the Token

    String getParameterName();

    // Get the specific Token value

    String getToken();

}

In the CsrfFilter class, we can find the processing logic for CsrfToken as shown here:

@Override

protected void doFilterInternal(HttpServletRequest request,

             HttpServletResponse response, FilterChain filterChain)

                     throws ServletException, IOException {

    request.setAttribute(HttpServletResponse.class.getName(), response);

    // Get CsrfToken from CsrfTokenRepository

    CsrfToken csrfToken = this.tokenRepository.loadToken(request);

    final boolean missingToken = csrfToken == null;

    // If CsrfToken is not found, generate one and save it to CsrfTokenRepository

    if (missingToken) {

        csrfToken = this.tokenRepository.generateToken(request);

        this.tokenRepository.saveToken(csrfToken, request, response);

    }

    // Add CsrfToken to the request

    request.setAttribute(CsrfToken.class.getName(), csrfToken);

    request.setAttribute(csrfToken.getParameterName(), csrfToken);

    if (!this.requireCsrfProtectionMatcher.matches(request)) {

        filterChain.doFilter(request, response);

        return;

    }

    // Get CsrfToken from the request

    String actualToken = request.getHeader(csrfToken.getHeaderName());

    if (actualToken == null) {

        actualToken = request.getParameter(csrfToken.getParameterName());

    }

    // If the CsrfToken carried in the request is different from the one obtained from the Repository, throw an exception

    if (!csrfToken.getToken().equals(actualToken)) {

        if (this.logger.isDebugEnabled()) {
...

              this.logger.debug("Invalid CSRF token found for "

                         + UrlUtils.buildFullRequestUrl(request));

             }

             if (missingToken) {

                 this.accessDeniedHandler.handle(request, response,

                         new MissingCsrfTokenException(actualToken));

             }

             else {

                 this.accessDeniedHandler.handle(request, response,

                         new InvalidCsrfTokenException(csrfToken, actualToken));

             }

             return;

        }

        

        // Continue with the filter chain in normal cases

        filterChain.doFilter(request, response);

}

The entire filter execution process is quite clear, basically centering around the verification of CsrfToken. We noticed that a CsrfTokenRepository is introduced here, which manages the storage of CsrfToken. This includes the CookieCsrfTokenRepository that focuses on handling cookies as mentioned earlier. From CookieCsrfTokenRepository, we can first see a group of constant definitions, including the cookie name, parameter name, and header name for CSRF as shown below:

static final String DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN";

static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";

static final String DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN";

The saveToken() method of CookieCsrfTokenRepository is also quite simple. It sets CsrfToken based on the Cookie object, as shown below:

@Override

public void saveToken(CsrfToken token, HttpServletRequest request,

         HttpServletResponse response) {

        String tokenValue = token == null ? "" : token.getToken();

        Cookie cookie = new Cookie(this.cookieName, tokenValue);

        cookie.setSecure(request.isSecure());

        if (this.cookiePath != null && !this.cookiePath.isEmpty()) {

                 cookie.setPath(this.cookiePath);

        } else {

                 cookie.setPath(this.getRequestContext(request));

        }

        if (token == null) {

             cookie.setMaxAge(0);

        }

        else {

             cookie.setMaxAge(-1);

        }

        cookie.setHttpOnly(cookieHttpOnly);

        if (this.cookieDomain != null && !this.cookieDomain.isEmpty()) {

             cookie.setDomain(this.cookieDomain);

        }

 

        response.addCookie(cookie);

}

In Spring Security, the CsrfTokenRepository interface has a number of implementations, including CookieCsrfTokenRepository and HttpSessionCsrfTokenRepository, which will not be explained in detail here.

After understanding the basic implementation process of CsrfFilter, let’s continue discussing how to use it to implement CSRF protection. Starting from Spring Security 4.0, CSRF protection is enabled by default to prevent CSRF attacks on applications. Spring Security CSRF protection is targeted at POST, PUT, and DELETE methods. Therefore, as a developer, you don’t actually need to do anything extra to use this feature. Of course, if you don’t want to use this feature, you can disable it using the following configuration method:

http.csrf().disable();

Customize CSRF Protection #

Based on the previous discussion, if you want to get the CsrfToken from the HTTP request, you just need to use the following code:

CsrfToken token = (CsrfToken)request.getAttribute("_csrf");

If you don’t want to use the built-in storage method of Spring Security and want to store CsrfToken based on your own needs, what you need to do is to implement the CsrfTokenRepository interface. Here we try to save CsrfToken to a relational database, so we can define a JpaTokenRepository by extending JpaRepository in Spring Data, as shown below:

public interface JpaTokenRepository extends JpaRepository<Token, Integer> {

 

    Optional<Token> findTokenByIdentifier(String identifier);

}

JpaTokenRepository is very simple, with only one query method to get Token based on the identifier, and the new interface is provided by the default JpaRepository, which we can directly use.

Then, we can build a DatabaseCsrfTokenRepository based on JpaTokenRepository, as shown below:

public class DatabaseCsrfTokenRepository implements CsrfTokenRepository {

    private JpaTokenRepository tokenRepository;

 

    public DatabaseCsrfTokenRepository(JpaTokenRepository tokenRepository) {

        this.tokenRepository = tokenRepository;

    }

 

    @Override

    public CsrfToken generateToken(HttpServletRequest request) {

        // generate token logic

    }

 

    @Override

    public void saveToken(CsrfToken token, HttpServletRequest request, 

                          HttpServletResponse response) {

        // save token logic

    }

 

    @Override

    public CsrfToken loadToken(HttpServletRequest request) {

        // load token logic

    }

 

}

In the DatabaseCsrfTokenRepository class, you can implement the generateToken(), saveToken(), and loadToken() methods to generate, save, and load CsrfToken respectively. The specific implementation can be based on your own business requirements.

// DatabaseCsrfTokenRepository class implementation
public class DatabaseCsrfTokenRepository implements CsrfTokenRepository {

    @Autowired
    private JpaTokenRepository jpaTokenRepository;

    @Override
    public CsrfToken generateToken(HttpServletRequest httpServletRequest) {
        String uuid = UUID.randomUUID().toString();
        return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", uuid);
    }

    @Override
    public void saveToken(CsrfToken csrfToken, HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
        String identifier = httpServletRequest.getHeader("X-IDENTIFIER");
        Optional<Token> existingToken = jpaTokenRepository.findTokenByIdentifier(identifier);

        if (existingToken.isPresent()) {
            Token token = existingToken.get();
            token.setToken(csrfToken.getToken());
        } else {
            Token token = new Token();
            token.setToken(csrfToken.getToken());
            token.setIdentifier(identifier);
            jpaTokenRepository.save(token);
        }
    }

    @Override
    public CsrfToken loadToken(HttpServletRequest httpServletRequest) {
        String identifier = httpServletRequest.getHeader("X-IDENTIFIER");
        Optional<Token> existingToken = jpaTokenRepository.findTokenByIdentifier(identifier);

        if (existingToken.isPresent()) {
            Token token = existingToken.get();
            return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token.getToken());
        }

        return null;
    }
}

The code for the class DatabaseCsrfTokenRepository is mostly self-explanatory. This class relies on the “X-IDENTIFIER” request header in the HTTP request to determine the unique identifier of the request and associate it with a specific CsrfToken. Then, we use the JpaTokenRepository to handle the persistence work for a relational database.

Finally, in order for the above code to take effect, we need to configure CSRF using the following method, which directly integrates our custom DatabaseCsrfTokenRepository through the csrfTokenRepository method:

@Override
protected void configure(HttpSecurity http) throws Exception {
        http.csrf(c -> {
            c.csrfTokenRepository(databaseCsrfTokenRepository());
        });
        ...
}

In summary, we can use the following diagram to outline the various components involved in customizing CSRF and their relationships:

Drawing 1.png

Customizing CSRF component diagram

Implementing CORS with Spring Security #

Having covered CSRF, let’s now look at another common requirement in web application development - CORS, or Cross-Origin Resource Sharing. So what is cross-origin?

What is CORS? #

In modern web application development, we typically adopt a frontend-backend separation approach, where data retrieval does not occur from the same origin. Therefore, cross-origin issues are very common in our day-to-day development. For example, when we initiate a request from the domain “test.com”, the browser, for security reasons, will not allow the request to access the domain “api.test.com” as it involves crossing two different domains.

Please note that cross-origin is a same-origin security policy imposed by the browser, so it only needs to be considered when the client runs in a browser. In terms of principles, the browser adds certain headers to the HTTP request’s message headers, as shown below:

// The origin domain set by the browser
Origin

// The HTTP methods the browser tells the server the request needs
Access-Control-Request-Method
// The browser tells the server which HTTP message headers are needed for the request

Access-Control-Request-Headers

When the browser makes a cross-origin request, it will perform a handshake protocol with the server. From the response, the following information can be obtained:

// Specifies which client domains are allowed to access this resource

Access-Control-Allow-Origin

// Supported HTTP methods by the server

Access-Control-Allow-Methods

// HTTP message headers that need to be included in the actual request

Access-Control-Allow-Headers

Therefore, the key to implementing CORS is the server. As long as the server sets these response message headers reasonably, it means that CORS support has been implemented, thus supporting cross-origin communication.

Using CorsFilter #

Similar to the CsrfFilter annotation, there is also a CorsFilter filter in Spring, but this filter is not provided by Spring Security, but comes from Spring Web MVC. In this CorsFilter filter, the first step should be to determine if the request from the client is a cross-origin request, and then determine whether the request is valid based on the CORS configuration. The code snippet below shows the process:

@Override

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,

             FilterChain filterChain) throws ServletException, IOException {

 

        if (CorsUtils.isCorsRequest(request)) {

             CorsConfiguration corsConfiguration = this.configSource.getCorsConfiguration(request);

             if (corsConfiguration != null) {

                 boolean isValid = this.processor.processRequest(corsConfiguration, request, response);

                 if (!isValid || CorsUtils.isPreFlightRequest(request)) {

                     return;

                 }

             }

        }

 

        filterChain.doFilter(request, response);

}

The above code creates the appropriate configuration class, CorsConfiguration. Similar to CorsFilter, Spring Security also provides the cors() method in the HttpSecurity utility class to create CorsConfiguration. Here is an example of how to use it:

@Override

protected void configure(HttpSecurity http) throws Exception {

        http.cors(c -> {

            CorsConfigurationSource source = request -> {

                CorsConfiguration config = new CorsConfiguration();

                config.setAllowedOrigins(Arrays.asList("*"));

                config.setAllowedMethods(Arrays.asList("*"));

                return config;

            };

            c.configurationSource(source);

        });

        …

}

We can use the setAllowedOrigins() and setAllowedMethods() methods to set the HTTP response message headers. Here, they are set to “*”, which means that all requests are allowed to make cross-origin access. You can also set specific domains and HTTP methods as needed.

Using @CrossOrigin annotation #

Through CorsFilter, we have implemented cross-origin settings at the global level. However, sometimes we may only need to implement this functionality for certain requests. Spring Security also allows us to do this by using the @CrossOrigin annotation on specific HTTP endpoints. Here is an example:

@Controller

public class TestController {

        

    @PostMapping("/hello")

	@CrossOrigin("http://api.test.com:8080")

    public String hello() {

        return "hello";

    }

}

By default, the @CrossOrigin annotation allows all domains and message headers, and maps the method in the Controller to all HTTP methods.

Summary and Preview #

This lecture focuses on the discussion of web request security. We discussed two common concepts in the development process: CSRF and CORS. These two concepts can sometimes be confusing, but they deal with two completely different scenarios.

CSRF is an attack behavior, so we need to protect the system. CORS, on the other hand, is more of a convention in front-end and back-end development. In Spring Security, corresponding filters are provided for these two scenarios, and we only need to configure them to automatically integrate the desired functionality into the system.

The main points of this lecture are as follows:

Drawing 2.png

Finally, I would like to leave you with a question to ponder: How can we customize a mechanism for handling CsrfToken in Spring Security? Feel free to share your thoughts in the comments.