再谈 CSRF 攻击应对之道

1,396 阅读3分钟

关于 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;
            }
        }
    }