【不完全翻译】Cross-Site Request Forgery (CSRF) Prevention Cheat Sheet

1,053 阅读18分钟

原文地址:owasp.org/www-project…

业余翻译,少部分章节有删减,并增加了一些个人总结。

简介

CSRF是一种恶意攻击,可能发生在网站、email、博客、讯息或者是一个程序。一旦用户登录过(经过授权),浏览器将执行一些用户意外的操作。CSRF攻击能起作用,是因为浏览器发出的请求自动包含了网站相关的授权认证(credentials),例如用户的cookie、IP地址。因此,如果用户被授权(登录过网站),网站无法区分合法或伪造的请求。我们需要一个攻击者无法获得的token或者标识(identifier),使其不会随着虚假请求被发送。

一个CSRF攻击的影响范围和应用所暴露的漏洞有关。例如,这个攻击可能利用用户的权限进行资金转移、密码更改或进行交易。CSRF攻击会在没有引起用户注意的前提下,通过目标浏览器去执行一些函数。

成功的CSRF攻击影响程度与受害者的权限相关。当攻击一个普通用户,用户数据和相关功能将被暴露;当攻击一个管理员,攻击将会使整个应用陷入危险;使用社交软件,攻击者可以将恶意HTML或js代码嵌入到email或网站中,从而用于给一个特定任务的URL发送请求。攻击任务将直接执行或间接利用XSS漏洞达成。

警告:确保没有XSS漏洞

XSS不是发起CSRF攻击的必须条件。但是却能用于击破CSRF的防范措施(除了下文提到的技术)。 这是因为XSS能使用 XMLHttpRequest简单读取任何网站上的页面 (如果在同一页面中可以直接访问DOM) ,然后从响应中获得token,再在伪造的请求中带上这个token。必须不存在XSS漏洞,以确保CSRF防御不被绕过。

需要注意发生CSRF攻击的地方

以下假设,是在你没有违反RFC2616,section9.1.1规定的前提下提出的。(即:不在GET操作中进行状态改变的操作)。 注意: 如果处于某些原因你在GET请求中进行了修改操作,你必须也要保护这些源头:比如src属性、href属性、form标签(GET方法)。

  • 使用POST请求的form tags
  • Ajax/XHR 调用

CSRF 防御概览

  • 1、基于token的防范措施
    • 抵御CSRF攻击,通常使用token的作为基础防范措施(有状态或无状态的)
  • 2、基于用户交互的防御措施
    • 涉及到高敏操作,还需要使用基于用户交互的保护措施(可以是重鉴权,或one-time token,下文详细展开)
  • 3、纵深防御策略(defense-in-depth )
    • defense-in-depth措施,一般需要配合基于token的防范措施,不可单独使用。

基础防范机制

一、基于token的防范措施

这是应用最广泛的CSRF措施之一。

  • token的2种实现方式:

    1. 有状态的

      同步token模式(synchronizer token pattern)

    2. 无状态的

      基于hash值或加密的token(我们推荐用AES256-GCM来加密, SHA256/512用于HMAC.)

  • 确保遵守2个准则:

    1. 请确保使用足够strong的加密或HMAC函数。注意:你可以选择任何你需要的算法,我们推荐使用AES256-GCM作为加密算法,SHA256/512 用于HMAC。
    2. 确保使用严格的密钥轮换(rotation key);给token设置生效时期。(可以在这里 查看OWASP的通用密钥管理指南)
  • 同步token模式(synchronizer token pattern)

    • 简介

      任何状态改变的操作(如post操作),都需要一个CSRF token。 我们鼓励开发者去使用这种同步token(这种同步token最初的目的是去检测form表单的重复提交)。 同步token的实施,要求生成与当前用户session相关的、随机的tokens。 这些tokens将被插入到form表单、高敏操作的相关调用。 服务端负责去校验token是否存在和准确性。通过将token添加到每个请求,开发者可以保证用户是有正确请求意图的。如果还要完成CSRF攻击,那攻击者必须要知道这个根据受害者session生成的随机token

      注意:这些token不像cookie,cookie会被自动添加到伪造请求中(这些请求由用户的浏览器发出)。这和攻击者能猜出目标受害者的session 标识类似。

    • 如何将token添加到请求中?

      当发起请求的时候,对于form表单,应该将token放入隐藏的input中;ajax请求时,添加到头部 、请求parameter中。token的值必须要是随机的,不能被攻击者猜到。确保token不会被服务端的日志或URL泄漏。一旦CSRF token验证失败,立即拒绝当前请求。

      对于java应用,可以考虑使用java.security.SecureRandom来生成足够长的随机token。可选的 生成算法包括256位 BASE64编码的哈希值。 开发者选择的生成算法必须保证token随机性和唯一性。

      总的来说,开发者只需要为当前session生成一次token。生成token之后,这个值将被存在sessi on里,被用在后续的请求中,直到session过期。 当一个请求被用户直接发出,服务端组件必须要验证token的存在性,并与用户session中的token对比一致。如果不存在,或者token验证不匹配,那么终止请求。

    • “每请求”token (pre-request token)

      如果想进一步提升安全性,可以考虑为每个请求都使用一个随机的CSRF token 参数名 或者 值。 此方法要求生成“每请求”token(per-request tokens),而不是“每会话”token(per-session tokens)。这比“每会话”token要更安全,因为token能被利用的时间范围是最小的。 不过,这可能会引起可用性问题。比如,后退按钮的功能将会失效。因为前一个页面的token不再有效,与上一页的交互将导致服务器上的CSRF误报安全事件。一些应用,比如银行,需要这种高安全性的方法。无论采用何种方法,都鼓励开发人员保护CSRF令牌,就像保护经过身份验证的会话标识符一样,例如使用TLS。

    • CSRF token的特点:

      1. 每个user session都是独立的
      2. 超大随机值,可由CSPRNG生成
      3. 一旦生成,存储在session中,直到session过期
      4. 这些tokens将被插入到HTML表单和高敏服务端操作相关调用。服务端程序负责验证token的存在与正确性。
    • 在头部带上CSRF token 更安全

      使用js在HTTP头部插入CSRF token通常比在form表单中添加隐藏字段更安全。

      在这个情况下,即使CSRF token被泄漏,但是攻击者依旧无法通过直接设置请求头来伪造post请求。

      这是因为,一旦攻击者尝试通过XMLHttpRequest设置任何自定义头部,浏览器将发出OPTIONS(pre-flight)请求。另外,当攻击者尝试使用flash来伪造头部,浏览器将发送一个到crossdomain.xml的get请求。这两种情况,浏览器都能阻止虚假请求被发送。

    • 被URL暴露的token:

      唯一的 per-session token(每session一个token) 在GET请求中将被暴露。GET请求有如下场景可泄漏CSRF token:

      浏览器历史、日志文件、用于记录HTTP请求首行的网络设备、referer头部(如果被保护网站连接到一个外部网站)。 想进一步提升安全性,可以将per-session token 做成per-request(每请求一个token)的。

  • 基于加密的Token模式(Encryption based Token Pattern)

    • token特点:

      这种token主要依靠加密算法的原理,而不是token的比对与验证。它适用于不保存任何状态的服务端。(比如分布式状态下,就更适合使用这种token)

    • token构造:

      服务端使用一个唯一的key,来加密用户的sessionId和时间戳(时间戳为了防止重放攻击,原文replay attacks)。这个token被返回给client,并嵌入到一个隐藏的表单字段,或者在AJAX请求的请求头请求参数

    • token验证:

      一旦接收到请求,服务端读取token,并继续使用创建阶段使用的key来解密token值。如果不能正确解密,意味着是一个攻击请求。解密之后,token里的用户sessionId 和时间戳被验证。sessionId与当前登录(发起请求)的用户session id作比对;时间戳用于与当前时间对比,验证token是否过期。

二、使用标准头部验证origin

  • 原理

    这种防御措施有2步,这2步都依赖于检查HTTP请求头的值。

    • 判断请求的来源。可以通过Origin头或者referer头。
    • 判断请求要去的目标origin。
    • 在server端我们验证这两个值是否匹配。如果他们匹配,则认为这个请求是合理的并接受这个请求(说明是同源请求);如果他们不匹配,我们阻止这个请求(意味这这个请求是跨域的)
  • 可靠性由什么保证?

    因为这些头部不能被程序代码修改(比如使用XSS中的javascript),只有浏览器有权设置他们。

  • 验证来源origin(Source Origin)

    • 通过Origin头部
      如果Origin头部存在,验证它的值是否和目标origin。不像Referer头部,从HTTPS来的HTTP请求(即:https的网站协议降级访问http的链接),将会保留Origin头部。
    • 通过Referer头部
      如果Origin头部不存在,通过判断Referer头的hostname是否和目标origin一致。这个防御方法通常被用于无授权的请求(unauthenticated request),比建立会话(session)前的请求,需要跟踪同步token(synchronization token)
    • 在这两种情况下,确保目标origin检测是完善的。举个例子,如果你的站点是site.com make,请确保site.com.attacker.com不会通过你的origin检测。(进一步说,用origin末尾的/ (斜杠)来保证站点origin的完全匹配)
  • 验证目标origin(Target Origin)

    初步的想法是从请求的URL中去抓取目标origin(hostname和port)。然而,应用的服务器通常经过了一个或多个代理,请求的原始URL一般和真正的服务器URL不同。如果你的应用服务器可被用户直接访问,那么使用URL中的origin是ok的。 你如果有用proxy,有几个选择可以考虑:

    • 在你的应用中,加一些配置以便能够简单地获得目标origin:这是你的应用,因此你可以找到它的目标origin 并在服务端的配置入口简单设置它的值。 这是最安全的方法,因为它由服务端设置,因此是个可信的值。 然而,如果你的应用会被发布到多个地方,比如dev、production、多生产实例,就会由问题了。 为每个情况设置正确的value是困难的,但是如果你可以通过某些中心配置,并让你的应用实例能从中获得值,那就太棒了。(确保这个中心化配置是安全的)
    • 使用Host头部: 如果你更倾向于让应用找到自己的target,那么就不需要为每个发布实例配置target。我们推荐使用Host族的头部。Host头部包含了请求的目标origin。但是你的app服务使用了proxy,Host头部可能会被代理修改,导致和原始请求的URL不同。这个更改,将导致无法与源Origin头部(或Referer 头部)匹配。
    • 使用X-Forwarded-Host头部: 为了解决代理更改host头部问题,我们可以使用另一个头部字段: X-Forwarded-Host。它的目的是为了保存proxy接收到的最原始Host头部值。大部分的代理将会直接转发 X-Forwarded-Host头部保存的目标origin值。因此,你可以使用这个字段,用于和来源origin的作比较。
  • 缺陷:

    • 少部分情况下,Origin头或Referrer头可能不存在(由于某些合法的原因,如保护用户隐私,或者浏览器的问题)

    • 向可信域发送CORS跨域请求,IE11不会增加Origin头部。Referer头部将只保留UI的origin

    • 302 重定向跨域, Origin将不会被包含在重定向的请求头部中,因为它们被认为是敏感信息不能被发送到其他域。

    • 某些涉及隐私的场景下,Origin被设置成“null”(www.google.com/search?q=or…

    • 对于同源请求,Origin头部一般都会带上。但是在大多数情况下,只会被POST/DELETE/PUT这些请求方法带上。如果你在GET请求中,做了一些修改状态的操作,那么这个方法将起不到原作用。

    • Referer 头部也不例外。 负载均衡、代理、嵌入式网络设备都因隐私问题,会导致referrer头部被删除。

三、Cookie的Samesite属性(Samesite Cookie Attribute)

  • 简介:

    SameSite 是一个cookie属性,用于阻止CSRF攻击。在RFC6265bis中被定义。在跨域请求的时候,这个属性帮助浏览器判断是否要发送cookies。可取的值有Lax、Strict和None。

  • 取值:

    • Strict值:

      在所有跨域请求甚至是链接跳转,都不会发送cookie。 比如在github上,一个已登录的用户,在讨论留言板中,点击一个会跳转到github私人项目的链接,那么cookie也不会被发送,无法访问次项目。 对于一些银行站点,任何交易相关的链接都不希望被任何外部站点引用,那么应该使用strict值

    • Lax值(默认值):

      Lax值提供了一个安全与可用性之间的平衡。从外部引用的链接跳转到本站,可以保持用户的登录状态;但是对于有CSRF倾向的post请求,将不会发送cookie。 Lax模式下允许的跨站请求,是最高级别的跳转模式 和同样安全的http请求方法。

    为了增强可靠性,应该配合token使用。

四、双重提交Cookie(Double Submit Cookie)

  • 简介:

    如果在服务端维护CSRF的状态是个问题,那么可以选择使用双重提交cookie。 这个方法简单易行,而且是无状态的。 在这个机制中,我们在cookie里存一个随机值,并同时作为一个请求的参数发送出去。服务端验证cookie里的值是否和请求参数是否一致。

  • 实现:

    用户访问时,站点应该生成伪随机值,并设置在cookie里。每次业务请求都将这个伪随机值提交,可包含在隐藏form表单字段、请求参数、请求头里。服务端来验证是否匹配。

  • 缺陷:

    这个方法,还需要确保子域名是安全的,并且确保都使用HTTPS。 通过双重提交,如果一个攻击者可以写一个cookie,他们就可以打破这个保护机制。写cookie远比 读他们更简单。同源策略规定一个站点的cookie不能被另一个站点访问(原文access)。但是有以下2种情形,跨域访问cookies是可能的:

    a) 子域名可写cookie,且发生XSS攻击

    由于同源策略,虽然 hellokitty.marketing.example.com 无法读取secure.example.com的cookies。但是hellokitty.marketing.example.com可以写父域名 (example.com)的cookie。这些cookies又被secure.example.com 所使用(无法区分cookie被哪个站点使用)。如果XSS发生在hellokitty.marketing.example.com,那么将有可能重写在secure.example.com中使用的cookies。

    b) 中间人攻击

    攻击者是中间攻击者,他们可以使用HTTP强制建立一个请求到相同domain。如果一个应用的host是https://secure.example.com, 即使cookies被设置了安全标志,中间人可以强制建立一个到http://secure.example.com连接,然后重写任何cookies。即使服务器设置了HSTS头部,并且使用的浏览器支持HSTS(这可以阻止中间人发送纯文字的http请求),中间人还是可以简单地给子域名发出一个请求,并重写cookies像a中所述。只有所有子站点都设置了HSTS头部,才是安全的。

    换句话说,只要http://hellokitty.marketing.example.com不强制使用https,攻击者就可以重写example.com subdomain的cookie。

    上面提到的场景a和b能发生,只有当CSRF token没有写到session。因此攻击者可以利用子域名发生的XSS漏洞,来重写父域名的cookie。使token和session(或授权cookie)发生联系(原文:linking token and session/auth cookie),将有助于避免这种漏洞。下面详细介绍:

    • 使用加密算法将cookie加密。

      • 将一个token存到加密的cookie中,而不是一个授权cookie(授权cookie总是被子域名共享)。在服务端通过解密cookie,然后判断得到的token是否与隐藏form表单(或请求头、请求参数)提交上来的token一致。
      • 原理:因为子域名没有父域名用于加密的密钥,因此无法正确重写cookie。
    • 带盐哈希

      • 一个更简单的cookie加密操作是,给token加一点盐来获得hash值(只有服务器端才知道的),然后再把这个token放到cookie
      • 这和加密整个cookie类似(都要求只有服务器掌握某些关键信息,如密钥),但是相比使用加密算法处理cookie,计算量更小一些。使用加密算法或者带盐哈希,攻击者将不能从字面token(plain token)再次构造出正确的cookie值,因为他们不知道服务器的密钥。

五、自定义头部(Use of Custom Request Headers)

CSRF token、双重提交cookie、加密token,或者其他和改变UI相关的防御手段,可能会增加复杂度。 一种适合AJAX/XHR的防御手段,是使用自定义请求头

  • 原理:

    同源策略(SOP)限制:只有同源的js脚本可以增加一个自定义的头部。 默认情况下,浏览器不允许js发出跨域请求。
  • 常用的头有“X-Requested-With: XMLHttpRequest”。因为某些js库已经默认把这个头部加到请求中,如AngularJS。 在这个情况下,你可以简单在服务器端判断这个头部是否存在并验证它的值。
  • 优点:

    • 不需要更改UI
    • 不需要引入服务端状态,更适合REST风格的服务。

你也可以增加你自己的自定义头部。这个防范机制将在4.3节详细介绍(CSRF的健壮防御).

  • 缺陷:

    • 早在2008年就有记录到,使用Flash来绕过防御。在2019年,使用老版本的firefox或者一些近期版本的Chrome也可以利用Flash来绕过防御。我们无法控制浏览器版本,因此这个技术不推荐作为一个基础防范措施。
    • 这种技术对AJAX调用有作用,但是仍然需要使用token来保护form标签。同时,CORS的相关配置也很重要,用于保证这个方法生效(来自外域请求的自定义头部,将触发一个CORS请求预检,原文pre-flight CORS check)。

六、基于用户交互的CSRF防范方法(User Interaction Based CSRF Defense)

前面提到的技术暂时还没有涉及到用户交互,有时跟用户交互相关的防御方法可能更简单也更合适。 以下有几种办法:

  • Re-Authentication (password or stronger)
  • One-time Token
  • CAPTCHA

虽然这是一个很强的CSRF防御,但是对用户体验有影响。对安全要求高的操作(如修改密码、转账),应配合token使用。 请注意,token本身就可以防御CSRF。开发者只有当面对高敏操作、追求更高的安全时,考虑使用这些用户交互相关的防御方式。

七、在AJAX请求头中使用JS自动添加CSRF tokens

下面的指引,默认GET、HEAD、OPTIONS方法都是安全操作。 因此,这三个方法的AJAX调用不需要追加CSRF token头部。 然而,如果这些方法执行了一些状态改变的操作,那么也需要增加CSRF token头部。 POST、PUT、PATCH、DELETE 和 TRACE 方法都是用于改变状态,应该添加CSRF token。

下面给出了一些指引,关于如何重写js库,以便于给每个AJAX请求自动增加CSRF tokens。

  • 在DOM中存储CSRF token的值

    一个CSRFtoken可以被存储在标签中。页面上所有的请求都可以从这个标签中提取出这个CSRF token。它也可以被存在JS 变量或者DOM中的任何地方。但是,它不建议被存在cookies或者local storage中。

    <meta name="csrf-token" content="{{ csrf_token() }}">

填充content属性的确切语法将取决于web应用程序的后端编程语言。

  • XMLHttpRequest (Native JavaScript) XMLHttpRequest 的open方法可以被重写,当open方法将被调用时,设置 anti-csrf-token头。下面定义的csrfSafeMethod方法将过滤所有安全方法,并给不安全的HTTP方法添加header
<script type="text/javascript">
    var csrf_token = document.querySelector("meta[name='csrf-token']").getAttribute("content");
    function csrfSafeMethod(method) {
        // these HTTP methods do not require CSRF protection
        return (/^(GET|HEAD|OPTIONS)$/.test(method));
    }
    var o = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function(){
        var res = o.apply(this, arguments);
        var err = new Error();
        if (!csrfSafeMethod(arguments[0])) {
            this.setRequestHeader('anti-csrf-token', csrf_token);
        }
        return res;
    };
 </script>