关于安全性:为什么在CSR中放置CSRF预防令牌很常见?

Why is it common to put CSRF prevention tokens in cookies?

我正试图理解CSRF的整个问题,以及预防它的适当方法。(我已阅读、理解并同意以下资源:OWASP CSRF预防措施摘要、有关CSRF的问题。)

据我所知,CSRF周围的漏洞是由以下假设引入的:(从Web服务器的角度来看)传入HTTP请求中的有效会话cookie反映了经过身份验证的用户的意愿。但是,源域的所有cookie都被浏览器神奇地附加到请求上,所以实际上,所有服务器都可以从请求中存在有效会话cookie推断出请求来自具有经过身份验证的会话的浏览器;它不能进一步假定有关在该浏览器中运行的代码的任何内容,或者是否真正反映了用户的意愿。防止这种情况发生的方法是在请求中包含额外的身份验证信息("CSRF令牌"),通过浏览器的自动cookie处理之外的其他方式携带。不严格地说,会话cookie对用户/浏览器进行身份验证,CSRF令牌对浏览器中运行的代码进行身份验证。

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

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

在AngularJS、Django、Rails中,将CSRF令牌作为cookie(即在一个set cookie头中)从服务器发送到客户机似乎很常见,然后让客户机中的javascript将其从cookie中刮掉,并将其作为单独的XSRF令牌头附加,以发送回服务器。

(另一种方法是由express推荐的方法,其中服务器生成的CSRF令牌通过服务器端模板扩展包含在响应正文中,直接附加到将其提供回服务器的代码/标记,例如作为隐藏表单输入。该示例是一种更具Web 1.0风格的处理方法,但它可以很好地概括为更重的JS客户机。)

为什么将set cookie用作CSRF令牌的下游传输如此常见/为什么这是个好主意?我想所有这些框架的作者都仔细考虑了他们的选择,没有弄错。但乍一看,使用cookie来解决cookie的设计限制似乎很愚蠢。事实上,如果您使用cookie作为往返传输(将cookie:header-downstream设置为服务器通知浏览器CSRF令牌,将cookie:header-upstream设置为浏览器返回服务器),您将重新引入您试图修复的漏洞。

我认识到上面的框架不使用cookie来完成CSRF令牌的整个往返过程;它们使用set cookie downstream,然后使用其他东西(例如x-csrf-token头),这确实关闭了漏洞。但是,即使使用set cookie作为下游传输,也有潜在的误导性和危险性;浏览器现在会将CSRF令牌附加到每个请求,包括真正的恶意XSRF请求;最多这会使请求比需要的更大,最坏的情况是,一些善意但误导的服务器代码可能会实际尝试使用iT,那就太糟糕了。而且,由于CSRF令牌的实际目标接收者是客户端的javascript,这意味着这个cookie不能只使用HTTP进行保护。因此,在set cookie头中向下游发送CSRF令牌对我来说似乎是非常不理想的。


一个很好的原因是,一旦收到CSRF cookie,就可以在客户端脚本的整个应用程序中使用它,以便在常规表单和Ajax日志中使用。这在JavaScript重的应用程序(如AngularJS使用的应用程序)中是有意义的(使用AngularJS不要求应用程序是单页应用程序,因此当状态需要在CSRF值通常无法在浏览器中保持的不同页面请求之间流动时,这将非常有用)。好的。

考虑典型应用程序中的以下场景和流程,了解您所描述的每种方法的优缺点。它们基于同步器令牌模式。好的。请求主体方法

  • 用户成功登录。
  • 服务器问题验证cookie。
  • 用户单击以导航到窗体。
  • 如果尚未为此会话生成,服务器将生成CSRF令牌,将其存储在用户会话中,并将其输出到隐藏字段。
  • 用户提交表单。
  • 服务器检查隐藏字段与会话存储令牌匹配。
  • 优势:好的。

    • 易于实现。
    • 使用Ajax。
    • 使用窗体。
    • cookie实际上只能是HTTP。

    缺点:好的。

    • 所有表单都必须以HTML格式输出隐藏字段。
    • 任何Ajax文章也必须包含该值。
    • 页面必须事先知道它需要CSRF令牌,以便将其包含在页面内容中,因此所有页面都必须在某个地方包含令牌值,这可能会使大型站点的实现耗时。

    自定义HTTP头(下游)

  • 用户成功登录。
  • 服务器问题验证cookie。
  • 用户单击以导航到窗体。
  • 页面在浏览器中加载,然后Ajax请求检索CSRF令牌。
  • 服务器生成CSRF令牌(如果尚未为会话生成),将其存储在用户会话中,并将其输出到标题。
  • 用户提交表单(令牌通过隐藏字段发送)。
  • 服务器检查隐藏字段与会话存储令牌匹配。
  • 优势:好的。

    • 使用Ajax。
    • cookie只能是http。

    缺点:好的。

    • 如果没有获得头值的Ajax请求,则无法工作。
    • 所有表单都必须动态地将值添加到其HTML中。
    • 任何Ajax文章也必须包含该值。
    • 页面必须先发出Ajax请求才能获得CSRF令牌,因此每次都意味着额外的往返。
    • 也可以直接将令牌输出到页面,这样可以保存额外的请求。

    自定义HTTP头(上游)

  • 用户成功登录。
  • 服务器问题验证cookie。
  • 用户单击以导航到窗体。
  • 如果还没有为此会话生成CSRF令牌,服务器将根据用户会话生成CSRF令牌,并将其输出到页面内容的某个位置。
  • 用户通过Ajax提交表单(令牌通过头部发送)。
  • 服务器检查自定义头与会话存储令牌匹配。
  • 优势:好的。

    • 使用Ajax。
    • cookie只能是http。

    缺点:好的。

    • 不适用于窗体。
    • 所有Ajax文章都必须包含标题。

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

  • 用户成功登录。
  • 服务器问题验证cookie。
  • 用户单击以导航到窗体。
  • 页面在浏览器中加载,然后Ajax请求检索CSRF令牌。
  • 服务器生成CSRF令牌(如果尚未为会话生成),将其存储在用户会话中,并将其输出到标题。
  • 用户通过Ajax提交表单(令牌通过头部发送)。
  • 服务器检查自定义头与会话存储令牌匹配。
  • 优势:好的。

    • 使用Ajax。
    • cookie只能是http。

    缺点:好的。

    • 不适用于窗体。
    • 所有Ajax文章还必须包含该值。
    • 页面必须先发出Ajax请求才能获得CRSF令牌,因此每次都意味着额外的往返。

    设置曲奇

  • 用户成功登录。
  • 服务器问题验证cookie。
  • 用户单击以导航到窗体。
  • 服务器生成CSRF令牌,将其存储在用户会话中,并将其输出到cookie。
  • 用户通过Ajax或HTML表单提交表单。
  • 服务器检查自定义头(或隐藏表单字段)是否与会话存储的令牌匹配。
  • 浏览器中的cookie可用于其他Ajax和表单请求,而无需向服务器发出检索CSRF令牌的其他请求。
  • 优势:好的。

    • 易于实现。
    • 使用Ajax。
    • 使用窗体。
    • 不一定需要Ajax请求来获取cookie值。任何HTTP请求都可以检索到它,并且可以通过JavaScript将其附加到所有表单/Ajax请求中。
    • 一旦检索到CSRF令牌(因为它存储在cookie中),就可以在不需要额外请求的情况下重用该值。

    缺点:好的。

    • 所有表单都必须动态地将值添加到其HTML中。
    • 任何Ajax文章也必须包含该值。
    • 对于每个请求(即所有不涉及CSRF过程的图像、CSS、JS等的GET),将提交cookie以增加请求大小。
    • cookie不能仅为http。

    因此,cookie方法是相当动态的,它提供了一种简单的方法来检索cookie值(任何HTTP请求)并使用它(JS可以自动将值添加到任何表单中,它可以作为头或表单值应用于Ajax请求中)。一旦为会话接收到CSRF令牌,就不需要重新生成它,因为使用CSRF漏洞的攻击者没有检索此令牌的方法。如果恶意用户试图在上述任何方法中读取用户的CSRF令牌,那么这将被同一源策略阻止。如果恶意用户试图检索CSRF令牌服务器端(例如,通过curl检索),那么该令牌将不会与请求中丢失的受害者身份验证会话cookie的同一用户帐户相关联(它将是攻击者的-因此不会与受害者的会话相关联)。好的。

    除了同步器令牌模式,还有双重提交cookie CSRF防止方法,当然,它使用cookie来存储一种CSRF令牌。这更容易实现,因为它不需要CSRF令牌的任何服务器端状态。使用此方法时,csrf令牌实际上可以是标准身份验证cookie,该值通常通过cookie与请求一起提交,但该值也会在隐藏字段或头中重复,攻击者无法复制该值,因为他们首先无法读取该值。但是,建议选择另一个cookie,而不是身份验证cookie,以便通过标记为httponly来保护身份验证cookie。所以这也是为什么你会发现使用基于cookie的方法预防CSRF的另一个常见原因。好的。好啊。


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

    攻击者将能够使用请求头中的auth token cookie和csrf cookie向服务器发出请求。但是服务器并没有在请求头中寻找CSRF令牌作为cookie,而是在请求的有效负载中寻找。即使攻击者知道把CSRF令牌放在哪里,他们也必须读取它的值才能放在那里。但是浏览器的跨源策略阻止从目标网站读取任何cookie值。

    同样的逻辑不适用于认证令牌cookie,因为服务器需要它在请求头中,攻击者不必做任何特殊的事情来将它放在那里。


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

  • 在请求主体(不是HTTP头)中。
  • 在自定义HTTP头中,不设置cookie。
  • 作为一个cookie,在一个set cookie头中。
  • 我认为第1个请求主体(尽管我在问题中链接的Express教程中演示了这一点)并不能移植到各种各样的情况下;并不是每个人都动态地生成每个HTTP响应;最终需要在生成的响应中放置令牌的地方可能会有很大的差异(在隐藏的表单输入中;在JSC的一个片段中)其他JS代码可以访问的ODE或变量;甚至在一个URL中,尽管这通常是放置CSRF令牌的一个不好的地方)。因此,虽然可以进行一些定制,1是一个很难做到一刀切的方法。

    第二个是定制头,它很有吸引力,但实际上不起作用,因为尽管JS可以为它调用的XHR获取头,但它无法获取它所加载页面的头。

    这就剩下了第三个cookie,一个set cookie头携带的cookie,作为一种在所有情况下都很容易使用的方法(任何人的服务器都可以设置每个请求cookie头,而请求主体中的数据类型无关紧要)。因此,尽管它有缺点,但它是框架广泛实现的最简单方法。


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

    我发现了一个解决方案,它在构建单页Web应用程序(SPA)时对我有用,其中包含许多Ajax请求。注意:我使用的是服务器端Java和客户端jQuery,但没有神奇的东西,所以我认为这个原理可以在所有流行的编程语言中实现。

    我的解决方案不需要额外的cookie很简单:

    客户端

    将服务器在成功登录后返回的CSRF令牌存储在全局变量中(如果您希望使用Web存储而不是全局存储,当然可以)。指示jquery在每个Ajax调用中提供一个x-csrf-token头。

    主"索引"页面包含此javascript代码段:

    1
    2
    3
    4
    5
    6
    7
    8
    // 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-form数据和get-json数据)以及捕获它们的服务器端过滤器位于a/dataservice/*路径下。登录请求不能点击过滤器,所以它们在另一个路径上。对HTML、CSS、JS和图像资源的请求也不在/dataservice/*路径上,因此不会被过滤。这些东西不含任何秘密,也不会造成任何伤害,所以这很好。

    1
    2
    3
    4
    5
    6
    7
    8
    @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);
    }