关于 CSRF 的攻击防御,网上已经给出了很多答案,我推荐 《CSRF 攻击的应对之道》。文章列举了三种方法,我就其中一种方法——在请求地址中添加 token 并验证,进行了改造,使它更符合我的需求。下面将详细记录改造的过程。
提示:请在阅读 《CSRF 攻击的应对之道》 之后再往下浏览
判断是否登入
通过 session 来判断用户是否登入,对已登入用户的操作进行保护看起来是最符合逻辑的方案。因为未登入时的操作均为公开属性。但是,如果网站采用了负载均衡,两台或更多服务器,session 中的 token 就不一样了,除非实现 session 共享。若解决上一个问题的方法会引发一个新问题,我通常建议放弃这个方法。
一般情况下,网站都会为已登入和未登入的类型分配不一样的权限和接口,比如:只有登入的用户才能发帖、修改个人信息。我们可以只保护这些表单免受 CSRF 攻击。
Spring MVC 框架
我使用的是 Spring MVC 框架,所以主要以 Spring MVC 为例,其他框架也类似,山转水不转。
现在我们的“国情”不同——不需要判断是否登入。若延续上述方法,则无法在初次打开网站时,向 session 中注入一个 token(将会直接验证),表单自然也无法获取该 token,最后的验证也是失败的。
我的建议是创建两个拦截器,分别拦截打开表单操作和提交表单操作。
第一个拦截器的工作内容及流程:
- 打开具有表单的页面时,生成一个 token,注入 session 中
- 页面完全打开
- token 转发到 form
- 后台不验证 token
第二个拦截器的工作内容及流程:
- 通过 request 将 token 转发到后台
- token 与 session.token 比较
- 相等则正常跳转,否则 403 报错
- 销毁 session.token
- 生成新的 session.token
表单设置隐藏元素,手动添加或通过js扫描自动添加均可
UML
CSRF 的攻击防御时序图
CSRF 的攻击防御类图
CSRFTokenManager 类包含生成 session token、获取表单 token、销毁 session token 类方法
package com.cn.csrf; import java.util.UUID; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import org.apache.commons.lang.StringUtils; /** * A manager for the CSRF token for a given session. The * {@link #getTokenForSession(HttpSession)} should used to obtain the token * value for the current session (and this should be the only way to obtain the * token value). * * @author Eyal Lupu * @update zhoupq_sh */ final class CSRFTokenManager { /** * The token parameter name */ static final String CSRF_PARAM_NAME = "CSRFToken"; /* * 生成 CSRFToken,并保存至session中 */ static String getTokenForSession(HttpSession session) { String token = null; synchronized (session) { token = (String) session.getAttribute("token"); if (!StringUtils.isNotBlank(token)) { token = UUID.randomUUID().toString(); session.setAttribute("token", token);// 将 token 保存至session中 } } return token; } /* * 通过request从页面的表单中获得 CSRFToken */ static String getTokenFromRequest(HttpServletRequest request) { return request.getParameter(CSRF_PARAM_NAME); } /* * 将 CSRFToken 从 session 中移除 */ static void RemoveSessionToken(HttpSession session) { session.removeAttribute("token"); } private CSRFTokenManager() { }; }
SaveCSRFHandlerInterceptor 类继承 HandlerInterceptorAdapter 拦截器,负责拦截进入含有表单的页面,并调用 CSRFTokenManager 中生成 seesion token的方法
package com.cn.csrf; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.lang.StringUtils; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; /** * 将生成的 CSRFToken 保存在带有表单提交的页面中 只保存 CSRFToken,不进行验证 * * @author zhoupq_sh * @date 2016-10-31 */ public class SaveCSRFHandlerInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { boolean isSaveToken = false; String CSRFToken = CSRFTokenManager.getTokenForSession(request .getSession()); if (StringUtils.isNotBlank(CSRFToken)) { isSaveToken = true; } return isSaveToken; } }
ValidateCSRFHandlerInterceptor 类继承 HandlerInterceptorAdapter 拦截器,负责拦截表单提交,获取并比较 session.token 和 request.token
package com.cn.csrf; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; public class ValidateCSRFHandlerInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // This is a POST request - need to check the CSRF tokena String sessionToken = CSRFTokenManager.getTokenForSession(request .getSession()); String requestToken = CSRFTokenManager.getTokenFromRequest(request); if (sessionToken.equals(requestToken)) { // 移除token CSRFTokenManager.RemoveSessionToken(request.getSession()); // url变化,重新生成 token CSRFTokenManager.getTokenForSession(request.getSession()); return true; } else { // response.sendError(HttpServletResponse.SC_FORBIDDEN,"Bad or missing CSRF value"); response.sendRedirect("/500.html"); return false; } } }