前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

1、CSRF

1.1、CSRF (Cross—Site Request Forgery:跨站点请求伪造)

攻击者盗用了你的身份,以你的名义发送恶意请求,对服务器来说这个请求是完全合法的,但是却完成了攻击者所期望的一个操作,比如以你的名义发送邮件、发消息,盗取你的账号,添加系统管理员,甚至于购买商品、虚拟货币转账等。

其中Web A为存在CSRF漏洞的网站,Web B为攻击者构建的恶意网站,User A为Web A网站的合法用户。

1、用户A打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A;在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A

2、用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B;

3、网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A;

4、浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行。

举例说明:

​ 受害者 A在银行有一笔存款,通过对银行的网站发送请求 http://bank.example/withdraw?account=A&amount=1000000&for=B可以使 A把 1000000 的存款转到 B的账号下。通常情况下,该请求发送到网站后,服务器会先验证该请求是否来自一个合法的 session,并且该 session 的用户 A已经成功登陆。

​ 黑客C自己在该银行也有账户,他知道某个 URL 可以把钱进行转帐操作。A可以自己发送一个请求给银行:http://bank.example/withdraw?account=A&amount=1000000&for=C。但是这个请求来自 C而非 A,他不能通过安全认证,因此该请求不会起作用。

​ 这时,C想到使用 CSRF 的攻击方式,他先自己做一个网站,在网站中放入如下代码: src=”http://bank.example/withdraw?account=A&amount=1000000&for=C ”,并且通过广告等诱使 Bob 来访问他的网站。

​ 当 A访问该网站时,上述 url 就会从 A的浏览器发向银行,而这个请求会附带 A浏览器中的 cookie 一起发向银行服务器。大多数情况下,该请求会失败,因为他要求 A的认证信息。但是,如果 A当时恰巧刚访问他的银行后不久,他的浏览器与银行网站之间的 session 尚未过期,浏览器的 cookie 之中含有 A的认证信息。这时,悲剧发生了,这个 url 请求就会得到响应,钱将从 A的账号转移到 C的账号,而 A当时毫不知情。等以后 A发现账户钱少了,即使他去银行查询日志,他也只能发现确实有一个来自于他本人的合法请求转移了资金,没有任何被攻击的痕迹。而 C则可以拿到钱后逍遥法外。

2、防御CSRF攻击

2.1、 验证 HTTP Referer 字段

2.1.1、方法

String referer = request.getHeader("referer");

根据 HTTP 协议,在 HTTP 头中有一个字段叫 referer,它记录了该 HTTP 请求的来源地址。在通常情况下,访问一个安全受限页面的请求来自于同一个网站,比如需要访问 http://bank.example/withdraw?account=A&amount=1000000&for=C,用户必须先登陆 bank.example,然后通过点击页面上的按钮来触发转账事件。这时,该转帐请求的 Referer 值就会是转账按钮所在的页面的 URL,通常是以 bank.example 域名开头的地址。而如果黑客要对银行网站实施 CSRF 攻击,他只能在他自己的网站构造请求,当用户通过黑客的网站发送请求到银行时,该请求的 Referer 是指向黑客自己的网站。

​ 因此,要防御 CSRF 攻击,银行网站只需要对于每一个转账请求验证其 Referer 值,如果是以 bank.example 开头的域名,则说明该请求是来自银行网站自己的请求,是合法的。如果 Referer 是其他网站的话,则有可能是黑客的 CSRF 攻击,拒绝该请求。

2.1.2、优点

​ 这种方法的显而易见的好处就是简单易行,网站的普通开发人员不需要操心 CSRF 的漏洞,只需要在最后给所有安全敏感的请求统一增加一个拦截器来检查 Referer 的值就可以。特别是对于当前现有的系统,不需要改变当前系统的任何已有代码和逻辑,没有风险,非常便捷。

2.1.3、缺点

​ 这种方法并非万无一失。Referer 的值是由浏览器提供的,虽然 HTTP 协议上有明确的要求,但是每个浏览器对于 Referer 的具体实现可能有差别,并不能保证浏览器自身没有安全漏洞。使用验证 Referer 值的方法,就是把安全性都依赖于第三方(即浏览器)来保障,从理论上来讲,这样并不安全。

  • 1、referer在一些浏览器上有可能被修改: 事实上,对于某些浏览器,比如 IE6 FF2,目前已经有一些方法可以篡改 Referer 。如果 bank.example 网站支持 IE6 浏览器,黑客完全可以把用户浏览器的 Referer 值设为以 bank.example 域名开头的地址,这样就可以通过验证,从而进行 CSRF 攻击。

  • 2、用户为了自己的隐私可能禁止referer传递:即便是使用最新的浏览器,黑客无法篡改 Referer 值,这种方法仍然有问题。因为 Referer 值会记录下用户的访问来源,有些用户认为这样会侵犯到他们自己的隐私权,特别是有些组织担心 Referer 值会把组织内网中的某些信息泄露到外网中。因此,用户自己可以设置浏览器使其在发送请求时不再提供 Referer。当他们正常访问银行网站时,网站会因为请求没有 Referer 值而认为是 CSRF 攻击,拒绝合法用户的访问。

  • 3、referer正常情况的访问也可能为空,不利于判断:

    • 1、页面从https跳转到http,从https的网站跳转到http的网站时,浏览器是不会发送referer

    • 2 、用户之间在浏览器中输入了要访问的目标URL(一般情况下的get请求)

2.1.4、总结

经过上面的优缺点,尤其是缺点3,我们正常输入地址也可能是referer为空,这个就很难把控,在不考虑缺点1、2的情况下,一般情况下设计到一些风险的操作一般不要写成get请求,无风险的get请求直接放行,我们可以设置为post请求,然后判断referer的有无,如果referer没有,那肯定不能通过,如果有了,则判断是不是当前域名

@Slf4j
public class RefererInterceptor extends HandlerInterceptorAdapter {

    private List<String> whiteList = Arrays.asList("aaaa.com", "bbb.com");

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse resp, Object o) throws Exception {

        String referer = request.getHeader("referer");
        String method = request.getMethod();
        //POST 必须有referer
        if (method.equals("POST")) {
            if (StringUtils.isBlank(referer)) {
                return false;
            }
            return validateReferer(referer, request);
        } else {
            if (StringUtils.isBlank(referer)) {
                return true;
            }
            return validateRefer(referer, request);
        }
    }

    public boolean validateReferer(String referer, HttpServletRequest request) {
        URL url = null;
        try {
            url = new URL(referer);
        } catch (MalformedURLException e) {
            return false;
        }
        String host = request.getServerName();
        if (!host.equals(url.getHost())) {
            if (whiteList.contains(url.getHost())) {
                return true;
            }
            return false;
        }
        return true;
    }
}

2.2、在请求地址中添加 token 并验证

使用token时候,尽量使用post请求代替get请求。防止token外泄。

​ ` CSRF 攻击之所以能够成功,重要请求的参数都是可以被攻击者猜测得到的。若是参数不能被攻击者所知,将无法实施CSRF攻击。要抵御 CSRF,**关键在于在请求中放入黑客所不能伪造的信息,并且该信息不存在于 cookie 之中。可以在 HTTP 请求中以参数的形式加入一个随机产生的 token,并在服务器端建立一个拦截器来验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF `攻击而拒绝该请求**。

​ 这种方法要比检查 Referer 要安全一些,token 可以在用户登陆后产生并放于 session 之中,然后在每次请求时把 token 从 session 中拿出,与请求中的 token 进行比对,但这种方法的难点在于如何把 token 以参数的形式加入请求。对于 GET 请求,token 将附在请求地址之后,这样 URL 就变成 http://url?csrftoken=tokenvalue。 而对于 POST 请求来说,要在 form 的最后加上 ,这样就把 token 以参数的形式加入请求了。但是,在一个网站中,可以接受请求的地方非常多,要对于每一个请求都加上 token 是很麻烦的,并且很容易漏掉,通常使用的方法就是在每次页面加载时,使用 javascript 遍历整个 dom 树,对于 dom 中所有的 a 和 form 标签后加入 token。这样可以解决大部分的请求,但是对于在页面加载之后动态生成的 html 代码,这种方法就没有作用,还需要程序员在编码时手动添加 token。

​ 该方法还有一个缺点是难以保证 token 本身的安全。特别是在一些论坛之类支持用户自己发表内容的网站,黑客可以在上面发布自己个人网站的地址(a标签)。由于系统也会在这个地址后面加上 token,黑客可以在自己的网站上得到这个 token,并马上就可以发动 CSRF 攻击。为了避免这一点,系统可以在添加 token 的时候增加一个判断,如果这个链接是链到自己本站的,就在后面添加 token,如果是通向外网则不加。不过,即使这个 csrftoken 不以参数的形式附加在请求之中,黑客的网站也同样可以通过 Referer 来得到这个 token 值以发动 CSRF 攻击(用户点击黑客的网站,自然会携带referer,如果get请求,很明显就会知道token的值了)。这也是一些用户喜欢手动关闭浏览器 Referer 功能的原因。

2.3、在 HTTP 头中自定义属性并验证,token的第二种方式

​ 这种方法也是使用 token 并进行验证,和上一种方法不同的是,这里并不是把 token 以参数的形式置于 HTTP 请求之中,而是把它放到 HTTP 头中自定义的属性里。通过 XMLHttpRequest 这个类,可以一次性给所有该类请求加上 csrftoken 这个 HTTP 头属性,并把 token 值放入其中。这样解决了上种2.2方法在请求中加入 token 的不便,同时,通过 XMLHttpRequest 请求的地址不会被记录到浏览器的地址栏,也不用担心 token 会透过 Referer 泄露到其他网站中去。

​ 然而这种方法的局限性非常大。 请求通常用于 Ajax 方法中对于页面局部的异步刷新,并非所有的请求都适合用这个类来发起,而且通过该类请求得到的页面不能被浏览器所记录下,从而进行前进,后退,刷新,收藏等操作,给用户带来不便。另外,对于没有进行 CSRF 防护的遗留系统来说,要采用这种方法来进行防护,要把所有请求都改为 请求,这样几乎是要重写整个网站,这代价无疑是不能接受的。

对于前后端分离的项目,这是一个非常不错的选择 。浏览器上看到的get地址都是前端的地址,而所有的向后台发起的请求地址都是在前端页面中请求的,浏览器直接输入,或者直接请求,不经过前端代码是获取不到token的

2.4、加验证码

验证码,强制用户必须与应用进行交互,才能完成最终请求。在通常情况下,验证码能很好遏制CSRF攻击。但是出于用户体验考虑,网站不能给所有的操作都加上验证码。因此验证码只能作为一种辅助手段,不能作为主要解决方案。

ContactAuthor