In the previous article, Songo talked about what a CSRF attack is and how to defend against it. I talked about several ways to deal with this problem in Spring Security.

Today Songo will take a brief look at Spring Security, CSRF defense source code.

This is the 19th article in this series. Reading the previous articles in this series will help you understand this article better:

  1. Dig a big hole and Spring Security will do it!
  2. How to decrypt the password
  3. A step-by-step guide to customizing form logins in Spring Security
  4. Spring Security does front and back separation, so don’t do page jumps! All JSON interactions
  5. Authorization in Spring Security used to be so simple
  6. How does Spring Security store user data into the database?
  7. Spring Security+Spring Data Jpa, Security management is only easier!
  8. Spring Boot + Spring Security enables automatic login
  9. Spring Boot automatic login. How to control security risks?
  10. How is Spring Security better than Shiro in microservices projects?
  11. Two ways for SpringSecurity to customize authentication logic (advanced play)
  12. How can I quickly view information such as the IP address of the login user in Spring Security?
  13. Spring Security automatically kicks out the previous login user.
  14. How can I kick out a user who has logged in to Spring Boot + Vue?
  15. Spring Security comes with a firewall! You have no idea how secure your system is!
  16. What is a session fixed attack? How do I defend against session fixation attacks in Spring Boot?
  17. How does Spring Security handle session sharing in a clustered deployment?
  18. Songgo hand in hand to teach you in SpringBoot CSRF attack! So easy!

This article is mainly explained from two aspects:

  1. Returned to the front end_csrfHow parameters are generated.
  2. From the front_csrfHow parameters are validated.

1. Random string generation

Let’s take a look at how the CSRF parameter is generated in Spring Security.

First, Spring Security provides a specification for storing CSRF parameters, namely CsrfToken:

public interface CsrfToken extends Serializable {
	String getHeaderName(a);
	String getParameterName(a);
	String getToken(a);

}
Copy the code

The first two methods get the key of the _csrf parameter, and the third one get the value of the _csrf parameter.

CsrfToken has two implementation classes as follows:

DefaultCsrfToken is used by default, so let’s look at DefaultCsrfToken a little bit:

public final class DefaultCsrfToken implements CsrfToken {
	private final String token;
	private final String parameterName;
	private final String headerName;
	public DefaultCsrfToken(String headerName, String parameterName, String token) {
		this.headerName = headerName;
		this.parameterName = parameterName;
		this.token = token;
	}
	public String getHeaderName(a) {
		return this.headerName;
	}
	public String getParameterName(a) {
		return this.parameterName;
	}
	public String getToken(a) {
		return this.token; }}Copy the code

This implementation is very simple, with few additional methods added, namely the implementation of interface methods.

The CsrfToken is the carrier of the _cSRF parameter. So how are parameters generated and saved? This involves another class:

public interface CsrfTokenRepository {
	CsrfToken generateToken(HttpServletRequest request);
	void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response);
	CsrfToken loadToken(HttpServletRequest request);
}
Copy the code

Here are three methods:

  1. The generateToken method is the CsrfToken generation process.
  2. The saveToken method saves the CsrfToken.
  3. LoadToken is how to load a CsrfToken.

CsrfTokenRepository has four implementation classes, two of which we used in the previous article: HttpSessionCsrfTokenRepository and CookieCsrfTokenRepository HttpSessionCsrfTokenRepository is the default.

Let’s look at HttpSessionCsrfTokenRepository implementation:

public final class HttpSessionCsrfTokenRepository implements CsrfTokenRepository {
	private static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";
	private static final String DEFAULT_CSRF_HEADER_NAME = "X-CSRF-TOKEN";
	private static final String DEFAULT_CSRF_TOKEN_ATTR_NAME = HttpSessionCsrfTokenRepository.class
			.getName().concat(".CSRF_TOKEN");
	private String parameterName = DEFAULT_CSRF_PARAMETER_NAME;
	private String headerName = DEFAULT_CSRF_HEADER_NAME;
	private String sessionAttributeName = DEFAULT_CSRF_TOKEN_ATTR_NAME;
	public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
		if (token == null) {
			HttpSession session = request.getSession(false);
			if(session ! =null) {
				session.removeAttribute(this.sessionAttributeName); }}else {
			HttpSession session = request.getSession();
			session.setAttribute(this.sessionAttributeName, token); }}public CsrfToken loadToken(HttpServletRequest request) {
		HttpSession session = request.getSession(false);
		if (session == null) {
			return null;
		}
		return (CsrfToken) session.getAttribute(this.sessionAttributeName);
	}
	public CsrfToken generateToken(HttpServletRequest request) {
		return new DefaultCsrfToken(this.headerName, this.parameterName,
				createNewToken());
	}
	private String createNewToken(a) {
		returnUUID.randomUUID().toString(); }}Copy the code

This source code is actually very easy to understand:

  1. The saveToken method stores the CsrfToken in the HttpSession and later takes notes from the HttpSession and the parameters passed in from the front end.
  2. The loadToken method of course reads the CsrfToken from HttpSession.
  3. GenerateToken is the process of generating a CsrfToken. As you can see, the generated default carrier is DefaultCsrfToken, and the CsrfToken value is generated by the createNewToken method, which is a UUID string.
  4. DefaultCsrfToken is constructed with two additional parameters, headerName and parameterName. These are the keys for the front-end save parameters.

This is the default scenario and is suitable for development where there is no front end or back end. See the previous article for details.

If you want to use in the front end separation development, another implementation class that requires CsrfTokenRepository CookieCsrfTokenRepository, code is as follows:

public final class CookieCsrfTokenRepository implements CsrfTokenRepository {
	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";
	private String parameterName = DEFAULT_CSRF_PARAMETER_NAME;
	private String headerName = DEFAULT_CSRF_HEADER_NAME;
	private String cookieName = DEFAULT_CSRF_COOKIE_NAME;
	private boolean cookieHttpOnly = true;
	private String cookiePath;
	private String cookieDomain;
	public CookieCsrfTokenRepository(a) {}@Override
	public CsrfToken generateToken(HttpServletRequest request) {
		return new DefaultCsrfToken(this.headerName, this.parameterName,
				createNewToken());
	}
	@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);
	}
	@Override
	public CsrfToken loadToken(HttpServletRequest request) {
		Cookie cookie = WebUtils.getCookie(request, this.cookieName);
		if (cookie == null) {
			return null;
		}
		String token = cookie.getValue();
		if(! StringUtils.hasLength(token)) {return null;
		}
		return new DefaultCsrfToken(this.headerName, this.parameterName, token);
	}
	public static CookieCsrfTokenRepository withHttpOnlyFalse(a) {
		CookieCsrfTokenRepository result = new CookieCsrfTokenRepository();
		result.setCookieHttpOnly(false);
		return result;
	}
	private String createNewToken(a) {
		returnUUID.randomUUID().toString(); }}Copy the code

Here than in HttpSessionCsrfTokenRepository _csrf data, are stored in the cookie, of course, when I read is read from the cookie, Elsewhere and HttpSessionCsrfTokenRepository is the same.

OK, that’s how we generated the entire _csrf argument.

To summarize, generate a CsrfToken, which is essentially a UUID string, and then store that Token in an HttpSession, or in a Cookie, and when the request comes in, Fetched from HttpSession or Cookie for validation.

2. Verify parameters

So the next step is the check.

Validation is mainly done through the CsrfFilter filter. Let’s look at the core doFilterInternal method:

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
				throws ServletException, IOException {
	request.setAttribute(HttpServletResponse.class.getName(), response);
	CsrfToken csrfToken = this.tokenRepository.loadToken(request);
	final boolean missingToken = csrfToken == null;
	if (missingToken) {
		csrfToken = this.tokenRepository.generateToken(request);
		this.tokenRepository.saveToken(csrfToken, request, response);
	}
	request.setAttribute(CsrfToken.class.getName(), csrfToken);
	request.setAttribute(csrfToken.getParameterName(), csrfToken);
	if (!this.requireCsrfProtectionMatcher.matches(request)) {
		filterChain.doFilter(request, response);
		return;
	}
	String actualToken = request.getHeader(csrfToken.getHeaderName());
	if (actualToken == null) {
		actualToken = request.getParameter(csrfToken.getParameterName());
	}
	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;
	}
	filterChain.doFilter(request, response);
}
Copy the code

Let me explain this a little bit:

  1. First call tokenRepository. LoadToken method reads CsrfToken, this tokenRepository is CsrfTokenRepository you configuration instance, The CsrfToken is stored in the HttpSession, which is read from the HttpSession, and the CsrfToken is stored in the Cookie, which is read from the Cookie.
  2. . If the call tokenRepository loadToken method is not loaded into the CsrfToken, that means this request may be initiated first, then call tokenRepository. CsrfToken generateToken approach, And call the tokenRepository. Save CsrfToken saveToken method.
  3. Notice that the request.setAttribute method is also called to store some values in it, and that’s what we do by default with JSP or thymeleaf tags_csrfData sources.
  4. RequireCsrfProtectionMatcher. Matches method is used to determine which request method needs to be done to check, by default, the “GET”, “the HEAD”, “TRACE”, “OPTIONS” method is not to need to check.
  5. The CSRF parameters passed in the request are then fetched from the request header or, if not, from the request parameters.
  6. After getting the CSRF parameter from the request, compare it with the csrfToken originally loaded, and throw an exception if it is different.

After this, the whole verification work is completed.

3.LazyCsrfTokenRepository

CsrfTokenRepository has four implementation classes, including LazyCsrfTokenRepository and LazyCsrfTokenRepository.

CsrfFilter does not require CSRF attack verification for common GET requests. However, whenever a GET request arrives, the following code executes:

if (missingToken) {
	csrfToken = this.tokenRepository.generateToken(request);
	this.tokenRepository.saveToken(csrfToken, request, response);
}
Copy the code

The CsrfToken is generated and saved, but it is virtually useless because THE GET request does not require CSRF attack verification.

As a result, Spring Security has officially launched LazyCS SrFtokenRepository.

LazyCsrfTokenRepository is not really a CsrfTokenRepository, it is a proxy, Can be used to enhance HttpSessionCsrfTokenRepository or CookieCsrfTokenRepository function:

public final class LazyCsrfTokenRepository implements CsrfTokenRepository {
	@Override
	public CsrfToken generateToken(HttpServletRequest request) {
		return wrap(request, this.delegate.generateToken(request));
	}
	@Override
	public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
		if (token == null) {
			this.delegate.saveToken(token, request, response); }}@Override
	public CsrfToken loadToken(HttpServletRequest request) {
		return this.delegate.loadToken(request);
	}
	private CsrfToken wrap(HttpServletRequest request, CsrfToken token) {
		HttpServletResponse response = getResponse(request);
		return new SaveOnAccessCsrfToken(this.delegate, request, response, token);
	}
	private static final class SaveOnAccessCsrfToken implements CsrfToken {
		private transient CsrfTokenRepository tokenRepository;
		private transient HttpServletRequest request;
		private transient HttpServletResponse response;

		private final CsrfToken delegate;

		SaveOnAccessCsrfToken(CsrfTokenRepository tokenRepository,
				HttpServletRequest request, HttpServletResponse response,
				CsrfToken delegate) {
			this.tokenRepository = tokenRepository;
			this.request = request;
			this.response = response;
			this.delegate = delegate;
		}
		@Override
		public String getToken(a) {
			saveTokenIfNecessary();
			return this.delegate.getToken();
		}
		private void saveTokenIfNecessary(a) {
			if (this.tokenRepository == null) {
				return;
			}

			synchronized (this) {
				if (this.tokenRepository ! =null) {
					this.tokenRepository.saveToken(this.delegate, this.request,
							this.response);
					this.tokenRepository = null;
					this.request = null;
					this.response = null;
				}
			}
		}

	}
}
Copy the code

Here, I would like to make three points:

  1. GenerateToken method, which is used to generate CsrfToken. The default carrier of CsrfToken is DefaultCsrfToken, now SaveOnAccessCsrfToken.
  2. There is not much difference between SaveOnAccessCsrfToken and DefaultCsrfToken, mainly the getToken method. In SaveOnAccessCsrfToken, When a developer calls getToken to retrieve the csrfToken, To do to csrfToken save operation (called HttpSessionCsrfTokenRepository or CookieCsrfTokenRepository saveToken method).
  3. LazyCsrfTokenRepository’s own saveToken has been modified, which means that the saveToken function has been abandoned. Calling this method does not save the repository.

LazyCsrfTokenRepository saves storage space by storing it only when csrfToken is used.

The configuration of LazyCsrfTokenRepository is also very simple. When we use Spring Security, if we do nothing to configure CSRF, The default is actually LazyCsrfTokenRepository + HttpSessionCsrfTokenRepository combination.

Of course, we can also configure their own, as follows:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests().anyRequest().authenticated()
            .and()
            .formLogin()
            .loginPage("/login.html")
            .successHandler((req,resp,authentication)->{
                resp.getWriter().write("success");
            })
            .permitAll()
            .and()
            .csrf().csrfTokenRepository(new LazyCsrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));
}
Copy the code

4. Summary

CSRF defense in Spring Security

In general, there are two ideas:

  1. The generated csrfToken is stored in HttpSession or Cookie.
  2. When the request arrives, the csrfToken is extracted from the request and compared with the saved csrfToken to determine whether the current request is valid.

Ok, do you guys GET it? If you feel that you have a harvest, remember to click under the encouragement of Songko oh ~