CSRF 攻击的对象
在讨论如何抵御 CSRF 之前,先要明确 CSRF 攻击的对象,也就是要保护的对象。从以上的例子可知,CSRF 攻击是黑客借助受害者的 cookie 骗取服务器的信任,但是黑客并不能拿到 cookie,也看不到 cookie 的内容。另外,对于服务器返回的结果,由于浏览器同源策略的限制,黑客也无法进行解析。因此,黑客无法从返回的结果中得到任何东西,他所能做的就是给服务器发送请求,以执行请求中所描述的命令,在服务器端直接改变数据的值,而非窃取服务器中的数据。所以,我们要保护的对象是那些可以直接产生数据改变的服务,而对于读取数据的服务,则不需要进行 CSRF的保护。比如银行系统中转账的请求会直接改变账户的金额,会遭到 CSRF 攻击,需要保护。而查询余额是对金额的读取操作,不会改变数据,CSRF 攻击无法解析服务器返回的结果,无需保护。\
Csrf攻击方式:
对象:A:普通用户,B:攻击者
1、假设A已经登录过xxx.com并且取得了合法的session,假设用户中心地址为:http://xxx.com/ucenter/index.do
2、B想把A余额转到自己的账户上,但是B不知道A的密码,通过分析转账功能发现xxx.com网站存在CSRF攻击漏洞和XSS漏洞。
3、B通过构建转账链接的URL如:http://xxx.com/ucenter/index.do?action=transfer&money=100000 &toUser=(B的帐号),因为A已经登录了所以后端在验证身份信息的时候肯定能取得A的信息。B可以通过xss或在其他站点构建这样一个URL诱惑A去点击或触发Xss。一旦A用自己的合法身份去发送一个GET请求后A的100000元人民币就转到B账户去了。当然了在转账支付等操作时这种低级的安全问题一般都很少出现。
防御CSRF:
1、验证 HTTP Referer 字段
2、在请求地址中添加 token 并验证
3、在 HTTP 头中自定义属性并验证
4、加验证码
(copy防御CSRF毫无意义,参考上面给的IBM专题的URL)
Token
最常见的做法是加token,Java里面典型的做法是用filter:https://code.google.com/p/csrf-filter/(链接由plt提供,源码上面的在:http://ahack.iteye.com/blog/1900708)
csrf-filter:
package com.google.code.csrf;
import java.io.IOException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.regex.Pattern;
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.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class StatelessCookieFilter implements Filter {
private final static Logger LOG = LoggerFactory.getLogger(StatelessCookieFilter.class);
private final static Pattern COMMA = Pattern.compile(",");
private String csrfTokenName;
private String oncePerRequestAttributeName;
private int cookieMaxAge;
private Set<String> excludeURLs;
private List<String> excludeStartWithURLs;
private Set<String> excludeFormURLs;
private Random random;
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpReq = (HttpServletRequest) req;
HttpServletResponse httpResp = (HttpServletResponse) resp;
if (httpReq.getAttribute(oncePerRequestAttributeName) != null) {
chain.doFilter(httpReq, httpResp);
} else {
httpReq.setAttribute(oncePerRequestAttributeName, Boolean.TRUE);
try {
doFilterInternal(httpReq, httpResp, chain);
} finally {
httpReq.removeAttribute(oncePerRequestAttributeName);
}
}
}
private void doFilterInternal(HttpServletRequest req, HttpServletResponse resp, FilterChain chain) throws IOException, ServletException {
if (!req.getMethod().equals("POST")) {
if (excludeFormURLs.contains(req.getServletPath())) {
chain.doFilter(req, resp);
return;
}
for (String curStart : excludeStartWithURLs) {
if (req.getServletPath().startsWith(curStart)) {
chain.doFilter(req, resp);
return;
}
}
String token = Long.toString(random.nextLong(), 36);
LOG.debug("new csrf token generated: {} path: {}", token, req.getServletPath());
req.setAttribute(csrfTokenName, token);
Cookie cookie = new Cookie(csrfTokenName, token);
cookie.setPath("/");
cookie.setMaxAge(cookieMaxAge);
resp.addCookie(cookie);
chain.doFilter(req, resp);
return;
}
if (excludeURLs.contains(req.getServletPath())) {
chain.doFilter(req, resp);
return;
}
String csrfToken = req.getParameter(csrfTokenName);
if (csrfToken == null) {
LOG.error("csrf token not found in POST request: {}", req);
if (!resp.isCommitted()) {
resp.sendError(400);
}
return;
}
req.setAttribute(csrfTokenName, csrfToken);
for (Cookie curCookie : req.getCookies()) {
if (curCookie.getName().equals(csrfTokenName)) {
if (curCookie.getValue().equals(csrfToken)) {
chain.doFilter(req, resp);
return;
} else {
LOG.error("mismatched csrf token. expected: {} received: {}", csrfToken, curCookie.getValue());
if (!resp.isCommitted()) {
resp.sendError(400);
}
return;
}
}
}
LOG.error("csrf cookie not found at: {}", req.getServletPath());
if (!resp.isCommitted()) {
resp.sendError(400);
}
}
public void destroy() {
// do nothing
}
public void init(FilterConfig config) throws ServletException {
String value = config.getInitParameter("csrfTokenName");
if (value == null || value.trim().length() == 0) {
throw new ServletException("csrfTokenName parameter should be specified");
}
csrfTokenName = value;
String excludedURLsStr = config.getInitParameter("exclude");
if (excludedURLsStr != null) {
String[] parts = COMMA.split(excludedURLsStr);
excludeURLs = new HashSet<String>(parts.length);
for (String cur : parts) {
excludeURLs.add(cur);
}
} else {
excludeURLs = new HashSet<String>(0);
}
String excludedFormURLsStr = config.getInitParameter("excludeGET");
if (excludedFormURLsStr != null) {
String[] parts = COMMA.split(excludedFormURLsStr);
excludeFormURLs = new HashSet<String>(parts.length);
for (String cur : parts) {
excludeFormURLs.add(cur.trim());
}
} else {
excludeFormURLs = new HashSet<String>(0);
}
String excludeStartWithURLsStr = config.getInitParameter("excludeGETStartWith");
if (excludeStartWithURLsStr != null) {
String[] parts = COMMA.split(excludeStartWithURLsStr);
excludeStartWithURLs = new ArrayList<String>(parts.length);
for (String curPart : parts) {
excludeStartWithURLs.add(curPart.trim());
}
} else {
excludeStartWithURLs = new ArrayList<String>(0);
}
String cookieMaxAgeStr = config.getInitParameter("cookieMaxAge");
if (cookieMaxAgeStr != null) {
try {
cookieMaxAge = Integer.parseInt(cookieMaxAgeStr);
} catch (NumberFormatException nfe) {
throw new ServletException("cookieMaxAge must be an integer: " + cookieMaxAgeStr, nfe);
}
} else {
cookieMaxAge = 3600; // 60*60 seconds = 1 hour
}
oncePerRequestAttributeName = getFirstTimeAttributeName();
random = new SecureRandom();
}
public static String getFirstTimeAttributeName() {
return StatelessCookieFilter.class.getName() + ".ATTR";
}
}
HowTo
- Configure web.xml:
<filter> <filter-name>csrfFilter</filter-name> <filter-class>com.google.code.csrf.StatelessCookieFilter</filter-class> <init-param> <param-name>csrfTokenName</param-name> <param-value>csrf</param-value> </init-param> <init-param> <!-- optional. urls to exclude from check --> <param-name>exclude</param-name> <param-value>/url1,/url/url2</param-value> </init-param> <init-param> <!-- optional. urls to exclude from generating csrf cookie. Useful for ajax requests that do not contain forms --> <param-name>excludeGET</param-name> <param-value>/url3,/url/url4</param-value> </init-param> <init-param> <!-- optional. urls to exclude from generating csrf cookie. Exclude do check servletPath().startsWith() --> <param-name>excludeGETStartWith</param-name> <param-value>/js/,/css/,/img/</param-value> </init-param> <init-param> <!-- optional. cookieMaxAge. By default 3600 seconds --> <param-name>cookieMaxAge</param-name> <param-value>18000</param-value> </init-param> </filter> <filter-mapping> <filter-name>csrfFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
- Add to every POST request parameter "csrf". For example form.jsp:
<form method="POST">
<input type="hidden" name="csrf" value="${csrf}">
</form>
- For "multipart/form-data" requests add to "action" attribute:
<form action="/url?csrf=${csrf}" method="POST" enctype="multipart/form-data">
<input type="file" name="file" size="50"/>
</form>