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

我试图理解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报头发送回服务器。

(另一种方法是例如表达所推荐的方法,其中服务器生成的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进行保护。因此,在Set-Cookie报头中向下游发送CSRF令牌对我来说似乎是次优的。

162642 次浏览

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

  1. 在请求体中(不是HTTP报头)。
  2. 在自定义HTTP报头中,而不是Set-Cookie。
  3. 作为一个cookie,在Set-Cookie报头中。

我认为第一个,请求体(虽然由我在问题中链接的Express教程演示),是不能移植到各种各样的情况;不是每个人都动态地生成每个HTTP响应;您最终需要在生成的响应中放置标记的位置可能差异很大(在隐藏的表单输入中;在JS代码片段或其他JS代码可访问的变量中;甚至可能在URL中,尽管这似乎是一个放置CSRF令牌的坏地方)。因此,虽然可以进行一些定制,但第一条很难采用一刀切的方法。

第二个,自定义头,是有吸引力的,但实际上不起作用,因为虽然JS可以获得它调用的XHR的标题,但它不能获得它加载的页面的标题

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

一个很好的理由(您已经提到过)是,一旦接收到CSRF cookie,就可以在整个应用程序的客户端脚本中使用它,用于常规表单和AJAX post。这在JavaScript较多的应用程序中是有意义的,比如AngularJS所使用的应用程序(使用AngularJS并不要求应用程序是一个单页应用程序,所以当状态需要在不同的页面请求之间流动,而CSRF值通常无法在浏览器中保存时,它会很有用)。

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

请求体方法

  1. 用户登录成功。
  2. 服务器发出认证cookie。
  3. 用户单击以导航到表单。
  4. 如果还没有为这个会话生成,服务器将生成CSRF令牌,将其存储在用户会话中,并将其输出到一个隐藏字段。
  5. 用户提交表单。
  6. 服务器检查隐藏字段是否与会话存储令牌匹配。

优点:

  • 易于实现。
  • 使用AJAX。
  • 使用表单。
  • Cookie实际上可以是HTTP只

缺点:

  • 所有表单都必须在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报头(upstream &下游)

  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也必须包含该值。
  • cookie将被提交给每一个请求(即图像,CSS, JS等的所有get,不涉及到CSRF过程),增加请求大小。
  • Cookie不能为HTTP只

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

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

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

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

同样的逻辑不适用于认证令牌cookie,因为服务器希望它出现在请求头中,攻击者不需要做任何特殊的事情就可以把它放在那里。

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

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

我没有额外饼干的解决方案很简单:

客户端

存储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 form-data和GET JSON-data)以及捕捉它们的服务器端过滤器位于/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);
}