好吧,这篇文章要说的是CSRF。我知道这已经是个很老的话题了,也没多少人对它感兴趣。但在做API/微服务架构防御的时候我觉得还是有些值得探讨的地方。
先搞个demo吧,以下是两个servlet,分别处理get和post请求:

@WebServlet("/data/get")
public class GetData extends HttpServlet {
    private static final long serialVersionUID = 1L;

    /**
     * @see HttpServlet#HttpServlet()
     */
    public GetData() {
        super();
        // TODO Auto-generated constructor stub
    }

    /**
     * @see HttpServlet#doGet(HttpServletRequest request, HttpServletResponse response)
     */
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.getWriter().print("Received data:"+request.getParameter("data"));
        System.out.println("Received data:"+request.getParameter("data"));
    }

}
@WebServlet("/data/post")
public class PostData extends HttpServlet {
    private static final long serialVersionUID = 1L;

    /**
     * @see HttpServlet#HttpServlet()
     */
    public PostData() {
        super();
        // TODO Auto-generated constructor stub
    }

    /**
     * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse response)
     */
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        response.getWriter().print("Received data:"+request.getParameter("data"));
        System.out.println("Received data:"+request.getParameter("data"));
    }

}

再来一个响应过滤器:

@WebFilter(filterName = "filter1")
public class CORSResponseFilter implements Filter {

    @Override
    public void destroy() {
        // TODO Auto-generated method stub

    }

    @Override
    public void doFilter(ServletRequest arg0, ServletResponse arg1,
            FilterChain arg2) throws IOException, ServletException {
        HttpServletResponse resp = (HttpServletResponse) arg1;
        HttpServletRequest request = (HttpServletRequest) arg0;
        resp.addHeader("Access-Control-Allow-Origin", "*");
        arg2.doFilter(request, resp);
    }

    @Override
    public void init(FilterConfig arg0) throws ServletException {
        // TODO Auto-generated method stub

    }

}

客户端代码:

<html>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script>
$.ajax({
   url: "http://localhost:8080/TestWeb/data/get",
   type: 'GET',
   data: { data: "get method" },
   success: function(data) {
    alert(data);

  },
  error: function(e) {
    alert("Error");
  }
});
</script>
<body>
</body>
</html>

对于大多数开发者来说,都知道用Access-Control-Allow-Origin来防御CSRF,但其实还有更多的问题需要考虑,比如:
1,你的配置是否正确。错误的Access-Control-Allow-Origin配置并不少见,但这不是我想讨论的。
2,就算正确配置了,万一本站或者子站存在XSS,依旧有可能造成CSRF。而这也不是我想讨论的。。
3,再者就是,就算用了Access-Control-Allow-Origin,你觉得就够了么?
我们先来做个实验,在响应过滤器中加入以下代码,那么通常的理解应该就是除了来自abc.com的请求,其他源发来的请求应该都无效。
resp.addHeader("Access-Control-Allow-Origin", "abc.com");
运行客户端代码,得到以下结果:

如预想的一样,无法得到请求结果。但你真的以为Access-Control-Allow-Origin阻止了这个请求么?完全没有。。。在服务器端的控制台可以看到如下输出:

如果发的是POST请求也会得到一样的结果:

POST请求代码:

<html>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script>
$.ajax({
   url: "http://localhost:8080/TestWeb/data/post",
   type: 'POST',
   data: { data: "post method" },
   success: function(data) {
    alert(data);

  },
  error: function(e) {
    alert("Error");
  }
});
</script>
<body>
</body>
</html>

也就是说,就算你正确配置了Access-Control-Allow-Origin,最多也就是防止别人从响应中偷数据。对于修改数据的请求完全没用,不知道有多少人意识到这点,我刚发现的时候觉得很是蛋疼。
之后呢,有人会说,restful API的GET请求本就该设计为不修改数据(有木有见过同一个service同时支持GETPOST的?),而POST请求的话内容应该是JSON,Content-Type也应该是application/json,而当不同源客户端修改Content-Typeapplication/json的时候浏览器会进行预检,之后请求就会失败。这个思路是对的,也在此提醒开发者,一定要在服务器端坚持Content-type,很多搞开发的会忘记这个。
然而,要考虑的不仅有这些。如果把其他响应头设置错误甚至会导致以上的防御失效。来看下一个例子,服务器端检查了所有POST请求的Content-type,必须存在且必须为application/json,允许任何源发OPTIONS请求,其他请求只允许abc.com发,但不小心在Access-Control-Allow-Headers中加入了Content-type
在响应过滤器代码中加入:

if (request.getMethod().equals("POST")
                && (request.getHeader("Content-Type") == null || !request
                        .getHeader("Content-Type").toLowerCase()
                        .startsWith("application/json"))) {
            resp.setStatus(HttpServletResponse.SC_TEMPORARY_REDIRECT);
            return;
        }
        if(request.getMethod().equals("OPTIONS")){
            resp.addHeader("Access-Control-Allow-Origin", "*");
        }
        else
        {
            resp.addHeader("Access-Control-Allow-Origin", "abc.com");
        }
        resp.addHeader("Access-Control-Allow-Headers", "Content-Type");
        resp.addHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");

客户端代码:

<html>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script>
$.ajax({
   url: "http://localhost:8080/TestWeb/data/post",
   type: 'POST',
   contentType: "application/json",
   data: JSON.stringify({ data: "post method" }),
   success: function(data) {
    alert(data);

  },
  error: function(e) {
    alert("Error");
  }
});
</script>
<body>
</body>
</html>

结果就是服务器端依旧接收到了这个请求并处理了:

数据值为null是因为客户端发的是JSON格式,服务器端需要特别处理一下才会显示。

至此,你可以看到,在做API CSRF防护的时候需要很谨慎,稍微配置错就可能导致防护失效。
首先你要确保GET请求就只是用来做数据获取的,而不是修改。
其次服务器端要检查Content-typ
然后在设置Access-Control-Allow-Origin的时候不仅要把域名范围考虑周全,还要把OPTIONS也考虑进去。
有人会说可以直接服务器端检查referral或者origin,这么做也是个解决方案,但并不是最佳实践。而如果遇到有要用API提供文件上传功能的时候,似乎也只能硬着头皮搞了,毕竟上传功能很可能需要支持multi-part,这种Content-type是不会触发预检的。
还有。。似乎在IE上预检的触发条件不一样。。。以上的例子在我机器上就没有触发。。
此文写出来仅是希望与大家共同探讨API安全,虽然现在微服务架构的应用还不多,但未来很可能成为趋势。

源链接

Hacking more

...