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值通常无法在浏览器中保持的不同页面请求之间流动时,这将非常有用)。好的。
考虑典型应用程序中的以下场景和流程,了解您所描述的每种方法的优缺点。它们基于同步器令牌模式。好的。请求主体方法
优势:好的。
- 易于实现。
- 使用Ajax。
- 使用窗体。
- cookie实际上只能是HTTP。
缺点:好的。
- 所有表单都必须以HTML格式输出隐藏字段。
- 任何Ajax文章也必须包含该值。
- 页面必须事先知道它需要CSRF令牌,以便将其包含在页面内容中,因此所有页面都必须在某个地方包含令牌值,这可能会使大型站点的实现耗时。
自定义HTTP头(下游)
优势:好的。
- 使用Ajax。
- cookie只能是http。
缺点:好的。
- 如果没有获得头值的Ajax请求,则无法工作。
- 所有表单都必须动态地将值添加到其HTML中。
- 任何Ajax文章也必须包含该值。
- 页面必须先发出Ajax请求才能获得CSRF令牌,因此每次都意味着额外的往返。
- 也可以直接将令牌输出到页面,这样可以保存额外的请求。
自定义HTTP头(上游)
优势:好的。
- 使用Ajax。
- cookie只能是http。
缺点:好的。
- 不适用于窗体。
- 所有Ajax文章都必须包含标题。
自定义HTTP头(上游和下游)
优势:好的。
- 使用Ajax。
- cookie只能是http。
缺点:好的。
- 不适用于窗体。
- 所有Ajax文章还必须包含该值。
- 页面必须先发出Ajax请求才能获得CRSF令牌,因此每次都意味着额外的往返。
设置曲奇
优势:好的。
- 易于实现。
- 使用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令牌服务器端(例如,通过
除了同步器令牌模式,还有双重提交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令牌从服务器下载到浏览器。
我认为第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); } |