我正在努力了解CSRF的整个问题以及防范它的适当方法。 (资源我已阅读,理解并同意:OWASP CSRF Prevention CHeat Sheet,Questions about CSRF。)
据我所知,围绕CSRF的漏洞是通过假设(从网络服务器的角度来看)传入HTTP请求中的有效会话cookie反映经过身份验证的用户的意愿而引入的。但是,原始域的所有cookie都被浏览器神奇地附加到请求上,因此实际上所有服务器都可以通过请求中存在的有效会话cookie来推断该请求来自具有经过身份验证的会话的浏览器;它无法进一步假设在该浏览器中运行的代码,或者它是否真正反映了用户的意愿。防止这种情况的方法是在请求中包含其他身份验证信息(“CSRF令牌”),这些信息由浏览器的自动cookie处理以外的某些方式携带。简而言之,会话cookie验证用户/浏览器,CSRF令牌验证浏览器中运行的代码。
简而言之,如果您使用会话cookie来验证Web应用程序的用户,则还应该为每个响应添加CSRF令牌,并在每个(变异)请求中要求匹配的CSRF令牌。然后,CSRF令牌从服务器到浏览器进行往返回服务器,向服务器证明发出请求的页面是由该服务器批准(甚至由该服务器生成)。
关于我的问题,这是关于该往返时该CSRF令牌使用的特定传输方法。
似乎很常见(例如在AngularJS,Django,Rails)将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令牌对我来说似乎非常不理想。
答案 0 :(得分:216)
有一个很好的理由,一旦收到CSRF cookie,它就可以在客户端脚本中的整个应用程序中使用,以便在常规表单和AJAX POST中使用。这在JavaScript重型应用程序中是有意义的,例如AngularJS使用的应用程序(使用AngularJS并不要求应用程序将是单页面应用程序,因此在状态需要在不同页面请求之间流动时,它将非常有用CSRF值通常不会在浏览器中持续存在。)
在典型应用程序中考虑以下场景和过程,以了解您描述的每种方法的优缺点。这些基于Synchronizer Token Pattern。
<强>优点:强>
<强>缺点:强>
<强>优点:强>
<强>缺点:强>
<强>优点:强>
<强>缺点:强>
<强>优点:强>
<强>缺点:强>
<强>优点:强>
<强>缺点:强>
因此,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令牌从服务器下载到浏览器。
我认为第一个,请求主体(虽然由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);
}