攻击原理
跨站点请求伪造(Cross Site Request Forgery,简称CSRF)能够形成的根本原因是利用了浏览器cookie自动发送的特点。如果浏览器cookie中保存了用户信息,该攻击利用了cookie共享机制获取了用户信息,因此可以正常通过系统的鉴权认证,实现非法操作。
下面以网上银行转账的例子来说明该攻击的流程。
1.小明在网上银行(bank.com)转账,浏览器cookie记录了小明的用户信息。
2. 小明在未关闭浏览器的情况下,又新建了页面开始浏览帖子(sns.com)。
3. 小黑是个黑客,已经将伪造的请求链接包含在sns.com的页面中。
4. 小明点中了小黑提供的伪造链接,那么该请求将会使用cookie中的合法信息,通过了银行的认证系统,实现小黑的非法操作。
这样就实现了跨站点请求伪造攻击。
防御措施
解决跨站点请求伪造攻击,一般有以下两种方法:
- Referer校验
- Token校验
2.1、referer校验
在 HTTP 协议的请求头部含有一个字段叫 Referer,它记录了本次请求的来源地址。例如从A网站跳到B网站,那么在B网站的看到的referer值就是A网站的域名。
在上面的银行转账例子的攻击中,银行系统接收到的攻击请求的referer是sns.com。如果该系统增加了referer校逻辑,就可以判断本次请求是否来自本系统bank.com,如果不是则拒绝。因此如
果银行系统看到请求的referer是sns.com,则可以拒绝访问。注意,第一次访问时,referer值会为空。
所以referer校验的策略是,允许referer值是本服务域名和空值的请求通过。
我们可以采用过滤器进行统一拦截和校验,代码如下:
package com.***.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.annotation.Order;
@Order(1)
@WebFilter(filterName = "CSRFFilter", urlPatterns = "/*")
public class CSRFFilter implements Filter {
private static final Logger logger = LoggerFactory.getLogger(CSRFFilter.class);
@Override
public void init(FilterConfig config) throws ServletException {
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
// System.out.println("==============CSRFFilter=================");
HttpServletRequest req = (HttpServletRequest) request;
HttpServletResponse resp = (HttpServletResponse) response;
if (checkReferer(req, resp)) {
// 拦截
response.setContentType("text/html;charset=utf-8");
logger.error("跨站点请求伪造攻击,系统已拦截");
throw new ServletException("非法请求!");
// return;
}
// 放行
chain.doFilter(req, resp);
}
private boolean checkReferer(HttpServletRequest request, HttpServletResponse response) {
String referer = request.getHeader("referer");
String serviceName = request.getServerName();
if (null == referer) {
return false;
}
if (referer != null && referer.matches("^(http|https)+://(" + serviceName + ")(.*)")) {
// 本系统的访问 直接放行
return false;
}
if (referer != null && referer
.matches("^[(http)|(https)]+://(localhost|10\\.|172\\.|127\\.0\\.0\\.1)+(.*)")) {
// 内网的访问 直接放行
return false;
}
logger.error("referer:{}", referer);
return true;
}
@Override
public void destroy() {
// TODO Auto-generated method stub
}
}
2.2、Token校验
token校验的原理是用户第一次登陆成功之后,系统给用户颁发随机且唯一的token,用户下次请求的时候,将该token作为参数传到后台,后台通过过滤器或者拦截器校验该token是否合法。若合法,则放行,不合法则拒绝访问。前端可以把token值存在请求头或者请求参数中,都可以。
后台校验逻辑如下:
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
HttpSession s = req.getSession();
// 从 session 中得到 csrftoken 属性
String sToken = (String) s.getAttribute("csrftoken");
if (sToken == null) {
// 产生新的 token 放在session 中
sToken = generateToken();
s.setAttribute("csrftoken", sToken);
chain.doFilter(request, response);
} else {
// 从 HTTP 头中取得 csrftoken
String xhrToken = req.getHeader("csrftoken");
// 从请求参数中取得 csrftoken
String pToken = req.getParameter("csrftoken");
if (sToken != null && xhrToken != null && sToken.equals(xhrToken)) {
chain.doFilter(request, response);
} else if (sToken != null && pToken != null && sToken.equals(pToken)) {
chain.doFilter(request, response);
} else {
request.getRequestDispatcher("error.jsp").forward(request, response);
}
}
}
```