Best Practices of Defending Cross-Site Request Forgery (CSRF) Attacks

By | June 25, 2020

What is CSRF?

CSRF is an abbreviation of Cross-Site Request Forgery. It is a script that can send HTTP requests. The victim can be disguised to send a request to the website to achieve the purpose of modifying the website data. CSRF can steal all user cookies after the user clicks a link so that the account is taken, some comments are inexplicably published. If it is a financial account, money can be stolen.

Principles of CSRF

When you log in to a website on your browser, the cookie will save the login information so that you don’t have to log in every time you continue to visit. CSRF uses this login state to send malicious requests to the backend. Why can the script get the cookie of the target website? This is because, as long as the target website is requested, the browser will automatically bring the cookie under the domain name of the website. The script below can prove that the malicious script can obtain the login information on the site. The premise is that you have logged into the website on your browser.

<!doctype html>
<html>
    <head>
        <meta charset="utf-8"/>
        <title>CSRF Defense Implementation</title>
    </head>
    <body>
        <span id="private_num"></span>
        <span id="private_num2"></span>
        <script>
            fetch('https://foo.bar/api/get', {
              credentials: 'include'  
            }).then(res => res.json())
            .then(
                res => {
                document.getElementById('private_num').innerText = res.data.fans_num;
                document.getElementById('private_num2').innerText = res.data.follow_num;
            })  
        </script>
    </body>
</html>

a CSRF experiment

Keep the website's login status and open the HTML file with a browser (we use Chrome in this article). You can see that this script has obtained my user information. Press F12 opens the cookie on the left side of the application selection column and some information about the current user from the website. This script allows each logged-in user to open, which is enough to show that the current user of the target website can be obtained and can send requests on behalf of the user. This is just a simple get request, what if it is a post request?

CSRF defense

There are generally three CSRF defense methods:

  1. SameSit Third-party websites are prohibited from using cookies on this site. This is when the backend sets the SameSite value to Strict or Lax when setting the cookie. When setting Strict, all requests on behalf of third-party websites cannot use cookies from this site. When setting up Lax, it means that only GET forms, "a" tags, and "link" tags of third-party websites are allowed to carry cookies. When None is set, it means the same as not set. It is currently only supported by Google Chrome.
@Bean
public CookieSerializer httpSessionIdResolver(){
    DefaultCookieSerializer cookieSerializer = new DefaultCookieSerializer();
    cookieSerializer.setCookieName("JESSIONID");
    cookieSerializer.setUseHttpOnlyCookie(true);
    cookieSerializer.setSameSite("Lax");
    cookieSerializer.setUseSecureCookie(true);
    return cookieSerializer;
}
  1. referer
    The referer represents the source of the request and cannot be forged.
    The back end writes a filter to check the referer in the request's header to verify whether it is a request for this website.
    The difference between referer and origin, only the post request will carry the origin request header, and the referer will take it in any case. The correct spelling of the referer should be a referrer, HTTP standard makers will make mistakes, do not plan to change. Please note that the browser can disable the referer feature.
  1. token
    The most common defense method is that the backend generates a token, puts it in the session, and sends it to the front end. The front-end carries this token when submitting a request. The backend determines whether it is a request of this website by checking whether the token and the token in the session are consistent. The user logs in, enters the account password, and requests the login interface. The backend puts the token in the session when the user login information is correct and returns the token to the front-end. The front-end stores the token in the localstory, and then sends the request to put the token in the header. Write a filter on the backend to intercept POST requests. Please pay attention to ignore requests that do not require tokens, such as logging in to the interface to obtain the token, so as not to check the token before getting it. Verification principle: the token in the session and the token in the front-end header are consistent, and the release is allowed.
@Slf4j
@Component
@WebFilter(urlPatterns = "/*", filterName = "verificationTokenFilter", description = "Check token")
public class VerificationTokenFilter implements Filter {

    List<String> ignorePathList = ImmutableList.of("/CSRF/login","/CSRF/getToken");

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) servletRequest;
        HttpServletResponse httpServletResponse = (HttpServletResponse) servletResponse;
        //ignore requests that do not require tokens
        String serviceUrl = httpServletRequest.getServletPath();
        for (final String ignorePath : ignorePathList) {
            if (serviceUrl.contains(ignorePath)) {
                filterChain.doFilter(servletRequest, servletResponse);
                return;
            }
        }
        String method = httpServletRequest.getMethod();
        if ("POST".equals(method)) {
           String tokenSession = (String)httpServletRequest.getSession().getAttribute("token");
           String token = httpServletRequest.getHeader("token");
            if (null != token && null != tokenSession && tokenSession.equals(token)) {
                filterChain.doFilter(servletRequest, servletResponse);
                return;
            } else {
                log.error("Invalid Token" + tokenSession + "!=" + token);
                httpServletResponse.sendError(403);
                return;
            }
        }
        filterChain.doFilter(servletRequest, servletResponse);
    }
    @Override
    public void destroy() {

    }
}