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>