为什么在CSR中放置CSRF预防令牌很常见?

时间:2013-12-10 20:45:32

标签: security cookies web csrf owasp

我正在努力了解CSRF的整个问题以及防范它的适当方法。 (资源我已阅读,理解并同意:OWASP CSRF Prevention CHeat SheetQuestions about CSRF。)

据我所知,围绕CSRF的漏洞是通过假设(从网络服务器的角度来看)传入HTTP请求中的有效会话cookie反映经过身份验证的用户的意愿而引入的。但是,原始域的所有cookie都被浏览器神奇地附加到请求上,因此实际上所有服务器都可以通过请求中存在的有效会话cookie来推断该请求来自具有经过身份验证的会话的浏览器;它无法进一步假设在该浏览器中运行的代码,或者它是否真正反映了用户的意愿。防止这种情况的方法是在请求中包含其他身份验证信息(“CSRF令牌”),这些信息由浏览器的自动cookie处理以外的某些方式携带。简而言之,会话cookie验证用户/浏览器,CSRF令牌验证浏览器中运行的代码。

简而言之,如果您使用会话cookie来验证Web应用程序的用户,则还应该为每个响应添加CSRF令牌,并在每个(变异)请求中要求匹配的CSRF令牌。然后,CSRF令牌从服务器到浏览器进行往返回服务器,向服务器证明发出请求的页面是由该服务器批准(甚至由该服务器生成)。

关于我的问题,这是关于该往返时该CSRF令牌使用的特定传输方法。

似乎很常见(例如在AngularJSDjangoRails)将CSRF令牌从服务器发送到客户端作为cookie(即在Set-Cookie标头中),以及然后在客户端中使用Javascript将其从cookie中删除并将其作为单独的XSRF-TOKEN标头附加以发送回服务器。

(另一种方法是例如Express推荐的方法,其中服务器生成的CSRF令牌通过服务器端模板扩展包含在响应正文中,直接附加到将提供的代码/标记它回到服务器,例如作为一个隐藏的表单输入。这个例子是一个更加web 1.0的方式做事,但会很好地概括为一个更重的JS客户端。)

为什么使用Set-Cookie作为CSRF令牌的下游传输是如此常见/为什么这是一个好主意?我想所有这些框架的作者都仔细考虑了他们的选择,并没有弄错。但乍一看,使用cookie解决基本上对cookie的设计限制似乎很愚蠢。实际上,如果您使用cookie作为往返传输(Set-Cookie:服务器的下游标头告诉浏览器CSRF令牌,而Cookie:上游标题,浏览器将其返回给服务器)您将重新引入漏洞正试图修复。

我意识到上面的框架不使用cookie来进行CSRF令牌的整个往返;他们使用Set-Cookie下游,然后在上游使用其他东西(例如X-CSRF-Token标头),这确实可以关闭漏洞。但即使使用Set-Cookie作为下游传输也可能具有误导性和危险性;浏览器现在将CSRF令牌附加到每个请求,包括真正的恶意XSRF请求;充其量只会使请求变得比它需要的更大,而在最坏的情况下,一些善意但误导的服务器代码实际上可能会尝试使用它,这将是非常糟糕的。此外,由于CSRF令牌的实际预期接收者是客户端Javascript,这意味着此cookie不能仅使用http保护。因此,在Set-Cookie标头中向下游发送CSRF令牌对我来说似乎非常不理想。

4 个答案:

答案 0 :(得分:216)

有一个很好的理由,一旦收到CSRF cookie,它就可以在客户端脚本中的整个应用程序中使用,以便在常规表单和AJAX POST中使用。这在JavaScript重型应用程序中是有意义的,例如AngularJS使用的应用程序(使用AngularJS并不要求应用程序将是单页面应用程序,因此在状态需要在不同页面请求之间流动时,它将非常有用CSRF值通常不会在浏览器中持续存在。)

在典型应用程序中考虑以下场景和过程,以了解您描述的每种方法的优缺点。这些基于Synchronizer Token Pattern

请求正文方法

  1. 用户成功登录。
  2. 服务器发出auth cookie。
  3. 用户点击以导航到表单。
  4. 如果尚未为此会话生成,则服务器会生成CSRF令牌,将其存储在用户会话中并将其输出到隐藏字段。
  5. 用户提交表单。
  6. 服务器检查隐藏字段匹配会话存储令牌。
  7. <强>优点:

    • 易于实施。
    • 适用于AJAX。
    • 使用表单。
    • Cookie实际上可以是HTTP Only

    <强>缺点:

    • 所有表单必须以HTML格式输出隐藏字段。
    • 任何AJAX POST都必须包含值。
    • 页面必须事先知道它需要CSRF令牌,因此它可以将其包含在页面内容中,因此所有页面都必须包含某处的令牌值,这可能会使实现大型站点的时间变得非常耗时。

    自定义HTTP标头(下游)

    1. 用户成功登录。
    2. 服务器发出auth cookie。
    3. 用户点击以导航到表单。
    4. 在浏览器中加载页面,然后发出AJAX请求以检索CSRF令牌。
    5. 服务器生成CSRF令牌(如果尚未为会话生成),将其存储在用户会话中并将其输出到 头。
    6. 用户提交表单(通过隐藏字段发送令牌)。
    7. 服务器检查隐藏字段匹配会话存储令牌。
    8. <强>优点:

      • 适用于AJAX。
      • Cookie可以是HTTP Only

      <强>缺点:

      • 没有AJAX请求就无法获取标头值。
      • 所有表单必须动态地将值添加到其HTML中。
      • 任何AJAX POST都必须包含值。
      • 页面必须首先发出一个AJAX请求才能获得CSRF令牌,因此这意味着每次额外往返。
      • 也可以简单地将令牌输出到页面,这将节省额外的请求。

      自定义HTTP标头(上游)

      1. 用户成功登录。
      2. 服务器发出auth cookie。
      3. 用户点击以导航到表单。
      4. 如果尚未为此会话生成,则服务器会生成CSRF令牌,将其存储在用户会话中,并将其输出到某处的页面内容中。
      5. 用户通过AJAX提交表单(令牌通过标头发送)。
      6. 服务器检查自定义标头匹配会话存储标记。
      7. <强>优点:

        • 适用于AJAX。
        • Cookie可以是HTTP Only

        <强>缺点:

        • 不使用表单。
        • 所有AJAX POST必须包含标题。

        自定义HTTP标头(上游和下游)

        1. 用户成功登录。
        2. 服务器发出auth cookie。
        3. 用户点击以导航到表单。
        4. 在浏览器中加载页面,然后发出AJAX请求以检索CSRF令牌。
        5. 服务器生成CSRF令牌(如果尚未为会话生成),将其存储在用户会话中并将其输出到 头。
        6. 用户通过AJAX提交表单(令牌通过标头发送)。
        7. 服务器检查自定义标头匹配会话存储标记。
        8. <强>优点:

          • 适用于AJAX。
          • Cookie可以是HTTP Only

          <强>缺点:

          • 不使用表单。
          • 所有AJAX POST还必须包含值。
          • 页面必须首先发出一个AJAX请求才能获得CRSF令牌,因此每次都需要额外的往返。

          设置Cookie

          1. 用户成功登录。
          2. 服务器发出auth cookie。
          3. 用户点击以导航到表单。
          4. 服务器生成CSRF令牌,将其存储在用户会话中并将其输出到cookie。
          5. 用户通过AJAX或HTML表单提交表单。
          6. 服务器检查自定义标头(或隐藏的表单字段)与会话存储的标记匹配。
          7. Cookie可在浏览器中使用,用于其他AJAX和表单请求,而无需向服务器请求检索CSRF令牌。
          8. <强>优点:

            • 易于实施。
            • 适用于AJAX。
            • 使用表单。
            • 不一定需要AJAX请求才能获取cookie值。任何HTTP请求都可以检索它,并且可以通过JavaScript将其附加到所有表单/ AJAX请求。
            • 一旦检索到CSRF令牌,因为它存储在cookie中,可以重复使用该值而无需其他请求。

            <强>缺点:

            • 所有表单必须动态地将值添加到其HTML中。
            • 任何AJAX POST都必须包含值。
            • 将为每个请求(即所有未参与CSRF流程的图像,CSS,JS等的GET)提交cookie,增加请求大小。
            • Cookie不能是HTTP Only

            因此,cookie方法是相当动态的,提供了一种简单的方法来检索cookie值(任何HTTP请求)并使用它(JS可以自动将值添加到任何表单中,并且可以作为标题在AJAX请求中使用它或作为表格价值)。一旦收到会话的CSRF令牌,就不需要重新生成它,因为使用CSRF漏洞的攻击者没有检索此令牌的方法。如果恶意用户试图在上述任何方法中读取用户的CSRF令牌,则Same Origin Policy将阻止这种情况。如果恶意用户尝试检索CSRF令牌服务器端(例如,通过curl),则此令牌将不会与同一用户帐户关联,因为请求中将缺少受害者的身份验证会话cookie(它将是攻击者 - 因此它不会与受害者的会话相关联服务器端。

            除了Synchronizer Token Pattern之外,还有Double Submit Cookie CSRF预防方法,当然使用cookie来存储一种CSRF令牌。这更容易实现,因为它不需要CSRF令牌的任何服务器端状态。实际上,CSRF令牌可以是使用此方法时的标准身份验证cookie,并且此值通常与请求一起通过cookie提交,但该值也会在隐藏字段或标头中重复,攻击者无法将其复制为他们首先无法读取价值。除了身份验证cookie之外,建议选择另一个cookie,以便通过标记为HttpOnly来保护身份验证cookie。因此,这是您使用基于cookie的方法找到CSRF预防的另一个常见原因。

答案 1 :(得分:36)

使用cookie向客户端提供CSRF令牌不允许成功攻击,因为攻击者无法读取cookie的值,因此无法将其放在服务器端CSRF验证所需的位置。

攻击者将能够使用请求标头中的身份验证令牌cookie和CSRF cookie向服务器发出请求。但是服务器没有在请求头中查找CSRF令牌作为cookie,它正在查看请求的有效负载。即使攻击者知道将CSRF令牌放在有效负载中的哪个位置,他们也必须读取它的值才能将其放在那里。但浏览器的跨域策略阻止从目标网站读取任何cookie值。

相同的逻辑不适用于身份验证令牌cookie,因为服务器在请求头中需要它,并且攻击者不必做任何特殊的事情就可以将它放在那里。

答案 2 :(得分:8)

我对答案的最佳猜测:考虑以下3个选项,了解如何将CSRF令牌从服务器下载到浏览器。

  1. 在请求正文中(不是HTTP标头)。
  2. 在自定义HTTP标头中,而不是Set-Cookie。
  3. 作为Cookie,在Set-Cookie标头中。
  4. 我认为第一个,请求主体(虽然由the Express tutorial I linked in the question展示),但对于各种各样的情况来说并不那么容易;并非所有人都动态生成每个HTTP响应;你最终需要在生成的响应中放置令牌的地方可能差异很大(在隐藏的表单输入中;在JS代码的片段中或其他JS代码可访问的变量;甚至可能在URL中看起来通常是一个不好的地方放置CSRF令牌)。因此,虽然可以通过一些自定义来实现,但#1是一个难以做到一刀切的方法。

    第二个,自定义标题,很有吸引力,但实际上并不起作用,因为while JS can get the headers for an XHR it invoked, it can't get the headers for the page it loaded from

    这留下了第三个,一个由Set-Cookie标头携带的cookie,作为一种易于在所有情况下使用的方法(任何人的服务器都能够设置每个请求的cookie头,并不重要请求体中有哪种数据)。因此,尽管它有缺点,但它是框架广泛实施的最简单方法。

答案 3 :(得分:0)

除了会话cookie(这是一种标准)之外,我不想使用额外的cookie。

我找到了一个解决方案,该解决方案在构建具有许多AJAX请求的单页Web应用程序(SPA)时适用。注意:我使用的是服务器端Java和客户端JQuery,但没有神奇的东西,因此我认为可以在所有流行的编程语言中实现这一原理。

没有多余Cookie的解决方案很简单:

客户端

将成功登录后服务器返回的CSRF令牌存储在全局变量中(当然,如果要使用Web存储而不是使用全局存储就可以了)。指示JQuery在每个AJAX调用中提供X-CSRF-TOKEN标头。

“索引”主页面包含以下JavaScript代码段:

// Intialize global variable CSRF_TOKEN to empty sting. 
// This variable is set after a succesful login
window.CSRF_TOKEN = '';

// the supplied callback to .ajaxSend() is called before an Ajax request is sent
$( document ).ajaxSend( function( event, jqXHR ) {
  jqXHR.setRequestHeader('X-CSRF-TOKEN', window.CSRF_TOKEN);
});

服务器端

成功登录后,创建一个随机(足够长)的CSRF令牌,将其存储在服务器端会话中,然后将其返回给客户端。通过将X-CSRF-TOKEN标头值与会话中存储的值进行比较来过滤某些(敏感)传入请求:这些请求应匹配。

敏感的AJAX调用(POST表单数据和GET JSON数据)以及捕获它们的服务器端过滤器位于/ dataservice / *路径下。登录请求一定不能点击过滤器,因此它们在另一个路径上。对HTML,CSS,JS和图像资源的请求也不在/ dataservice / *路径上,因此未过滤。这些没有任何秘密,也不会造成伤害,所以很好。

@WebFilter(urlPatterns = {"/dataservice/*"})
...
String sessionCSRFToken = req.getSession().getAttribute("CSRFToken") != null ? (String) req.getSession().getAttribute("CSRFToken") : null;
if (sessionCSRFToken == null || req.getHeader("X-CSRF-TOKEN") == null || !req.getHeader("X-CSRF-TOKEN").equals(sessionCSRFToken)) {
  resp.sendError(401);
} else 
  chain.doFilter(request, response);
}