将CSRF预防令牌放在cookie中为什么很常见?


283

我正在尝试了解CSRF的整个问题以及适当的预防方法。(我已阅读,理解并同意的资源:OWASP CSRF预防速查表关于CSRF的问题。)

据我了解,CSRF周围的漏洞是由以下假设引入的:从Web服务器的角度来看,传入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:上游标头将浏览器返回给服务器),则会重新引入此漏洞正在尝试修复。

我意识到上述框架并未在CSRF令牌的整个往返过程中使用cookie;他们在下游使用Set-Cookie,然后在上游使用其他内容(例如X-CSRF-Token标头),这确实消除了该漏洞。但是,即使使用Set-Cookie作为下游运输工具,也可能会产生误导和危险;浏览器现在将CSRF令牌附加到每个请求,包括真正的恶意XSRF请求;最好的情况是使请求大于所需的大小,最坏的情况是一些善意却被误导的服务器代码实际上可能会尝试使用它,这确实很糟糕。而且,由于CSRF令牌的实际预期接收者是客户端Javascript,这意味着该cookie不能仅通过http保护。


找到正确的位置是一个很好的问题。
kta

Answers:


261

您碰到的一个很好的理由是,一旦收到CSRF cookie,就可以在客户端脚本中的整个应用程序中使用它,以常规形式和AJAX POST形式使用。这在AngularJS所使用的JavaScript繁重的应用程序中是有意义的(使用AngularJS不需要该应用程序将是单个页面应用程序,因此在状态需要在不同页面请求之间流动且CSRF值需要流动的情况下将很有用)通常无法在浏览器中保留)。

考虑典型应用程序中的以下方案和过程,以了解您描述的每种方法的利弊。这些基于“ 同步器令牌模式”

请求正文方法

  1. 用户成功登录。
  2. 服务器发出身份验证Cookie。
  3. 用户单击以导航到表单。
  4. 如果尚未为此会话生成,则服务器将生成CSRF令牌,将其存储在用户会话中,并将其输出到隐藏字段。
  5. 用户提交表格。
  6. 服务器检查隐藏字段与会话存储的令牌是否匹配。

优点:

  • 易于实现。
  • 与AJAX一起使用。
  • 使用表格。
  • Cookie实际上可以是HTTP Only

缺点:

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

自定义HTTP标头(下游)

  1. 用户成功登录。
  2. 服务器发出身份验证Cookie。
  3. 用户单击以导航到表单。
  4. 页面加载到浏览器中,然后发出AJAX请求以检索CSRF令牌。
  5. 服务器生成CSRF令牌(如果尚未为会话生成),将其存储在用户会话中,并将其输出到标头。
  6. 用户提交表单(令牌通过隐藏字段发送)。
  7. 服务器检查隐藏字段与会话存储的令牌是否匹配。

优点:

  • 与AJAX一起使用。
  • Cookie可以是仅HTTP

缺点:

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

自定义HTTP标头(上游)

  1. 用户成功登录。
  2. 服务器发出身份验证Cookie。
  3. 用户单击以导航到表单。
  4. 如果尚未为此会话生成,则服务器将生成CSRF令牌,将其存储在用户会话中,并将其输出到页面内容中的某个位置。
  5. 用户通过AJAX提交表单(令牌通过标头发送)。
  6. 服务器检查自定义标头与会话存储的令牌是否匹配。

优点:

  • 与AJAX一起使用。
  • Cookie可以是仅HTTP

缺点:

  • 不适用于表格。
  • 所有AJAX POST必须包含标头。

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

  1. 用户成功登录。
  2. 服务器发出身份验证Cookie。
  3. 用户单击以导航到表单。
  4. 页面加载到浏览器中,然后发出AJAX请求以检索CSRF令牌。
  5. 服务器生成CSRF令牌(如果尚未为会话生成),将其存储在用户会话中,并将其输出到标头。
  6. 用户通过AJAX提交表单(令牌通过标头发送)。
  7. 服务器检查自定义标头与会话存储的令牌是否匹配。

优点:

  • 与AJAX一起使用。
  • Cookie可以是仅HTTP

缺点:

  • 不适用于表格。
  • 所有AJAX POST也必须包含该值。
  • 该页面必须首先发出AJAX请求才能获得CRSF令牌,因此每次都意味着一次额外的往返。

Set-Cookie

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

优点:

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

缺点:

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

因此,cookie方法是相当动态的,提供了一种检索cookie值(任何HTTP请求)和使用它的简便方法(JS可以自动将该值添加到任何形式,并且可以在AJAX请求中用作标头或表单值)。一旦已为会话接收到CSRF令牌,就无需重新生成它,因为采用CSRF攻击的攻击者没有检索此令牌的方法。如果恶意用户尝试以上述任何一种方法读取用户的CSRF令牌,则将通过Same Origin Policy阻止。如果恶意用户尝试检索CSRF令牌服务器端(例如,通过curl),则此令牌将不会与同一用户帐户相关联,因为受害者的auth会话cookie将在请求中丢失(这将是攻击者的,因此不会在服务器端与受害者的会话相关联)。

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


7
我不确定我是否可以安全地完成“发出AJAX请求以检索CSRF令牌”(“自定义标头:下游”部分中的第4步)的方法;由于这是一个单独的请求,因此服务器不知道它来自谁。怎么知道泄露CSRF令牌是安全的?在我看来,如果您无法从初始页面加载中获取令牌,则会丢失(不幸的是,这使自定义下游响应标头成为了非启动者)。
metamatt 2013年

6
因为伪造者将没有会话cookie。他们可能有自己的会话cookie,但是由于CSRF令牌与会话相关联,因此其CSRF令牌与受害者的令牌不匹配。
SilverlightFox

32
以我对CSRF攻击的理解,伪造者确实拥有我的会话cookie。好吧,他们实际上并没有看到 cookie,但是他们能够在伪造的请求中提供它,因为这些请求来自我的浏览器,而我的浏览器提供了我的会话cookie。因此,从服务器的角度来看,仅会话cookie不能将合法请求与伪造请求区分开。实际上,这是我们要阻止的攻击。顺便说一句,BTW感谢您耐心等待,特别是如果我对此感到困惑时。
metamatt

8
他们具有提供auth cookie的能力,但是他们无法读取包含CSRF令牌的响应。
SilverlightFox

8
@metamatt对不起,坏死了,但我会为那些迷路的人做。据我所知,攻击者通常无法获得响应。CSRF主要用于引起副作用,而不是直接收集数据。例如,CSRF攻击脚本可能会强制特权用户提升攻击者的特权,禁用安全设置或强制已登录的Paypal用户将转移发送到特定的电子邮件地址。在以上任何一种情况下,攻击者都不会关心响应,该响应仍会发送到受害者的浏览器。只有进攻的结果。
jonathanbruder

61

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

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

相同的逻辑不适用于auth令牌cookie,因为服务器期望它在请求标头中,并且攻击者无需执行任何特殊操作即可将其放置在那里。


当然,攻击者无需首先读取cookie。他们可以将图像插入被src='bank.com/transfer?to=hacker&amount=1000浏览器请求的被黑客入侵的网站上,并附上该网站的相关Cookie bank.com
developius

2
CSRF用于在客户端验证用户,而不是通常按照您的建议保护站点免受服务器端损害。
Tongfa

2
@developius发送cookie不足以满足CSRF保护。Cookie包含服务器发送的csrf令牌。合法客户端必须从cookie中读取csrf令牌,然后将其传递到请求中的某个位置,例如标头或有效负载中。CSRF保护检查cookie中的值是否与请求中的值匹配,否则请求被拒绝。因此,攻击者确实需要读取cookie。
Will M.

1
这个答案非常指向原始海报的问题,并且非常清楚。+1谢谢。
java-addict301

@Tongfa-谢谢,这有助于我更好地理解。我是否应该假设CSRF令牌不应该放在标题中?它一定在身体某处吗?
zerohedge

10

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

  1. 在请求正文中(不是HTTP标头)。
  2. 在自定义HTTP标头中,而不是Set-Cookie中。
  3. 作为Cookie,在Set-Cookie标头中。

我认为第一个请求体(在我链接到问题中的Express教程中进行了演示)并不是很容易适应各种情况;并非每个人都动态地生成每个HTTP响应。最终需要在生成的响应中放入令牌的地方可能会相差很大(以隐藏形式输入;在JS代码的片段中或其他JS代码可访问的变量中;甚至在URL中,尽管这通常看起来很糟糕)放置CSRF令牌)。因此,尽管可以进行一些自定义,但#1很难做到一种千篇一律的方法。

第二个是自定义标头,很吸引人,但实际上不起作用,因为尽管JS可以获取它所调用的XHR的标头,但它无法获取从中加载的页面的标头

这留下了第三个,即由Set-Cookie标头携带的cookie,这是一种在所有情况下都易于使用的方法(任何人的服务器都可以设置每个请求的cookie标头,而无论哪种类型数据在请求正文中)。因此,尽管有缺点,但它是框架广泛实施的最简单方法。


7
我可能会说的很明显,这是否意味着cookie不能是httponly正确的?
Photon

1
仅用于ajax请求(JS需要知道csrf cookie的值才能在第二个通道中的下一个请求(作为表单数据或标头)上重新发送它)。如果会话cookie已经是HttpOnly(以防止XSS攻击),则没有理由要求csrf令牌为HttpOnly,因为如果没有关联的会话,csrf令牌本身并不有价值。
Cowbert

2

除了会话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);
}   

我认为您希望在登录请求中使用CSRF。您似乎也将CSRF令牌用作登录会话令牌。它还可以使它们成为单独的令牌,然后,无论用户是否登录,都可以在任何端点上使用CSRF。
Tongfa
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.