最近项目渗透测试检测出一些安全问题其中一项为csrf攻击隐患,然后开始修复
csrf简介
CSRF(Cross-site request forgery)跨站请求伪造,也被称为“One Click Attack”或者Session Riding,通常缩写为CSRF或者XSRF,是一种对网站的恶意利用。尽管听起来像跨站脚本(XSS),但它与XSS非常不同,XSS利用站点内的信任用户,而CSRF则通过伪装成受信任用户的请求来利用受信任的网站。与XSS攻击相比,CSRF攻击往往不大流行(因此对其进行防范的资源也相当稀少)和难以防范,所以被认为比XSS更具危险性。——百度百科
一、maven pom.xml 引入jar包
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>5.0.6.RELEASE</version>
</dependency>
二、web.xml配置filter
mgmtCsrfFilter是我重写的csrfFilter名称
<!-- CSRF filter -->
<filter>
<filter-name>mgmtCsrfFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>mgmtCsrfFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
三、spring-application.xml配置
<bean id="mgmtCsrfFilter" class="com.haha.sps.mgmt.core.filter.MgmtCsrfFilter">
<constructor-arg>
<bean class="org.springframework.security.web.csrf.HttpSessionCsrfTokenRepository" />
</constructor-arg>
</bean>
<bean id="requestDataValueProcessor" class="org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor"></bean>
四、重写csrffilter
重写后的csrfFilter为MgmtCsrfFilter代码如下:
package com.haha.sps.mgmt.core.filter;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfTokenRepository;
import org.springframework.security.web.csrf.InvalidCsrfTokenException;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
import org.springframework.security.web.util.UrlUtils;
import org.springframework.security.web.util.matcher.RegexRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.web.filter.OncePerRequestFilter;
/**
* Rewrite org.springframework.security.web.csrf.CsrfFilter
* exclude user/doLogion method filter
*
* @author Pente
*/
public final class MgmtCsrfFilter extends OncePerRequestFilter {
/**
* The default {@link RequestMatcher} that indicates if CSRF protection is required or
* not. The default is to ignore GET, HEAD, TRACE, OPTIONS and process all other
* requests.
*/
public static final RequestMatcher DEFAULT_CSRF_MATCHER = new CsrfSecurityRequestMatcher();
private final Log logger = LogFactory.getLog(getClass());
private final CsrfTokenRepository tokenRepository;
private RequestMatcher requireCsrfProtectionMatcher = DEFAULT_CSRF_MATCHER;
private AccessDeniedHandler accessDeniedHandler = new AccessDeniedHandlerImpl();
public MgmtCsrfFilter(CsrfTokenRepository csrfTokenRepository) {
Assert.notNull(csrfTokenRepository, "csrfTokenRepository cannot be null");
this.tokenRepository = csrfTokenRepository;
}
/*
* (non-Javadoc)
*
* @see
* org.springframework.web.filter.OncePerRequestFilter#doFilterInternal(javax.servlet
* .http.HttpServletRequest, javax.servlet.http.HttpServletResponse,
* javax.servlet.FilterChain)
*/
@Override
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);
}
/**
* Specifies a {@link RequestMatcher} that is used to determine if CSRF protection
* should be applied. If the {@link RequestMatcher} returns true for a given request,
* then CSRF protection is applied.
*
* <p>
* The default is to apply CSRF protection for any HTTP method other than GET, HEAD,
* TRACE, OPTIONS.
* </p>
*
* @param requireCsrfProtectionMatcher the {@link RequestMatcher} used to determine if
* CSRF protection should be applied.
*/
public void setRequireCsrfProtectionMatcher(
RequestMatcher requireCsrfProtectionMatcher) {
Assert.notNull(requireCsrfProtectionMatcher,
"requireCsrfProtectionMatcher cannot be null");
this.requireCsrfProtectionMatcher = requireCsrfProtectionMatcher;
}
/**
* Specifies a {@link AccessDeniedHandler} that should be used when CSRF protection
* fails.
*
* <p>
* The default is to use AccessDeniedHandlerImpl with no arguments.
* </p>
*
* @param accessDeniedHandler the {@link AccessDeniedHandler} to use
*/
public void setAccessDeniedHandler(AccessDeniedHandler accessDeniedHandler) {
Assert.notNull(accessDeniedHandler, "accessDeniedHandler cannot be null");
this.accessDeniedHandler = accessDeniedHandler;
}
/*
* exclude "user/doLogion" "GET", "HEAD", "TRACE", "OPTIONS"
*/
private static final class CsrfSecurityRequestMatcher implements RequestMatcher {
private RegexRequestMatcher unprotectedMatcher = new RegexRequestMatcher("^/user/doLogin", null);
private final HashSet<String> allowedMethods = new HashSet<String>(
Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
/*
* (non-Javadoc)
* @see
* org.springframework.security.web.util.matcher.RequestMatcher#matches(javax.
* servlet.http.HttpServletRequest)
*/
@Override
public boolean matches(HttpServletRequest request) {
if(this.allowedMethods.contains(request.getMethod())){
return false;
}
return !unprotectedMatcher.matches(request);
}
}
}
实现自定义过滤掉登录方法,主要是重写csrfFilter里的DefaultRequiresCsrfMatcher替换成自己写的CsrfSecurityRequestMatcher ,如下:
/*
* exclude "user/doLogion" "GET", "HEAD", "TRACE", "OPTIONS"
*/
private static final class CsrfSecurityRequestMatcher implements RequestMatcher {
private RegexRequestMatcher unprotectedMatcher = new RegexRequestMatcher("^/user/doLogin", null);
private final HashSet<String> allowedMethods = new HashSet<String>(
Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS"));
/*
* (non-Javadoc)
* @see
* org.springframework.security.web.util.matcher.RequestMatcher#matches(javax.
* servlet.http.HttpServletRequest)
*/
@Override
public boolean matches(HttpServletRequest request) {
if(this.allowedMethods.contains(request.getMethod())){
return false;
}
return !unprotectedMatcher.matches(request);
}
}
五、页面配置
在post请求或ajax请求中会遇到问题,在公用jsp文件中加上,例如每个页面都会引用的头文件top.jsp
<meta name="_csrf" content="${_csrf.token}"/>
<meta name="_csrf_header" content="${_csrf.headerName}"/>
<script>
var token = $("meta[name='_csrf']").attr("content");
var header = $("meta[name='_csrf_header']").attr("content");
$.ajaxSetup({
beforeSend: function (xhr) {
if(header && token ){
xhr.setRequestHeader(header, token);
}
}}
);
// $(document).ajaxSend(function(e,xhr,opt){
// xhr.setRequestHeader(header,token);
// });
</script>
$.ajaxSetup的意思就是给我们所有的请求都加上这个header和token,或者放到form表单中。注意,_csrf这个要与spring security的配置文件中的配置相匹配,默认为_csrf。
六、源码解析
看一下CsrfFilter中的doFilterInternal源码解析就知道我们为什么这么修改了
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
request.setAttribute(HttpServletResponse.class.getName(), response);
// 先从tokenRepository中加载token
CsrfToken csrfToken = this.tokenRepository.loadToken(request);
final boolean missingToken = csrfToken == null;
// 如果为空,则tokenRepository生成新的token,并保存到tokenRepository中
if (missingToken) {
csrfToken = this.tokenRepository.generateToken(request);
this.tokenRepository.saveToken(csrfToken, request, response);
}
// 将token写入request的attribute中,方便页面上使用
request.setAttribute(CsrfToken.class.getName(), csrfToken);
request.setAttribute(csrfToken.getParameterName(), csrfToken);
//这个macher就是我们在Spring配置文件中自定义的过滤器,也就是GET,HEAD, TRACE, OPTIONS和我们的rest都不处理
// 这个macher就是我们在Spring配置文件中自定义的过滤器,
//如果不需要csrf验证的请求,则直接下传请求(requireCsrfProtectionMatcher是默认的对象,对符合^(GET|HEAD|TRACE|OPTIONS)$的请求和我们自定义的请求不验证)
if (!this.requireCsrfProtectionMatcher.matches(request)) {
filterChain.doFilter(request, response);
return;
}
// 从用户请求中获取token信息
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);
}