我试图理解CSRF的整个问题和适当的方法来防止它。(我已经阅读、理解并同意的资源:OWASP CSRF预防小抄,关于CSRF的问题)

As I understand it, the vulnerability around CSRF is introduced by the assumption that (from the webserver's point of view) a valid session cookie in an incoming HTTP request reflects the wishes of an authenticated user. But all cookies for the origin domain are magically attached to the request by the browser, so really all the server can infer from the presence of a valid session cookie in a request is that the request comes from a browser which has an authenticated session; it cannot further assume anything about the code running in that browser, or whether it really reflects user wishes. The way to prevent this is to include additional authentication information (the "CSRF token") in the request, carried by some means other than the browser's automatic cookie handling. Loosely speaking, then, the session cookie authenticates the user/browser and the CSRF token authenticates the code running in the browser.

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

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

这看起来很常见(例如在AngularJS, Django, Rails中),将CSRF令牌作为一个cookie从服务器发送到客户端(即在Set-Cookie报头中),然后让客户端Javascript从cookie中抓取它,并将其作为一个单独的XSRF-TOKEN报头发送回服务器。

(另一种方法是Express推荐的方法,其中服务器生成的CSRF令牌通过服务器端模板展开包含在响应体中,直接附加到将其提供给服务器的代码/标记上,例如作为隐藏的表单输入。这个例子是一种更接近web 1.0的做事方式,但可以推广到一个更偏重于js的客户端。)

Why is it so common to use Set-Cookie as the downstream transport for the CSRF token / why is this a good idea? I imagine the authors of all these frameworks considered their options carefully and didn't get this wrong. But at first glance, using cookies to work around what's essentially a design limitation on cookies seems daft. In fact, if you used cookies as the roundtrip transport (Set-Cookie: header downstream for the server to tell the browser the CSRF token, and Cookie: header upstream for the browser to return it to the server) you would reintroduce the vulnerability you are trying to fix.

I realize that the frameworks above don't use cookies for the whole roundtrip for the CSRF token; they use Set-Cookie downstream, then something else (e.g. a X-CSRF-Token header) upstream, and this does close off the vulnerability. But even using Set-Cookie as the downstream transport is potentially misleading and dangerous; the browser will now attach the CSRF token to every request including genuine malicious XSRF requests; at best that makes the request bigger than it needs to be and at worst some well-meaning but misguided piece of server code might actually try to use it, which would be really bad. And further, since the actual intended recipient of the CSRF token is client-side Javascript, that means this cookie can't be protected with http-only. So sending the CSRF token downstream in a Set-Cookie header seems pretty suboptimal to me.


当前回答

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

其他回答

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

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

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

A good reason, which you have sort of touched on, is that once the CSRF cookie has been received, it is then available for use throughout the application in client script for use in both regular forms and AJAX POSTs. This will make sense in a JavaScript heavy application such as one employed by AngularJS (using AngularJS doesn't require that the application will be a single page app, so it would be useful where state needs to flow between different page requests where the CSRF value cannot normally persist in the browser).

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

请求体方法

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

优点:

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

缺点:

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

自定义HTTP报头(下行)

用户登录成功。 服务器发出认证cookie。 用户单击以导航到表单。 页面在浏览器中加载,然后发出AJAX请求来检索CSRF令牌。 服务器生成CSRF令牌(如果尚未为会话生成),根据用户会话存储它并将其输出到 头。 用户提交表单(令牌通过隐藏字段发送)。 服务器检查隐藏字段是否与会话存储令牌匹配。

优点:

使用AJAX。 Cookie可以是HTTP Only。

缺点:

如果没有AJAX请求来获取头值,则无法工作。 所有表单都必须将值动态地添加到其HTML中。 任何AJAX post也必须包含该值。 页面必须首先发出AJAX请求以获得CSRF令牌,因此这意味着每次都要进行额外的往返。 还不如简单地将令牌输出到页面,这样就可以节省额外的请求。

自定义HTTP报头(上游)

用户登录成功。 服务器发出认证cookie。 用户单击以导航到表单。 如果还没有为这个会话生成,服务器将生成CSRF令牌,将其存储在用户会话中,并将其输出到页面内容的某处。 用户通过AJAX提交表单(令牌通过报头发送)。 服务器检查自定义报头匹配会话存储令牌。

优点:

使用AJAX。 Cookie可以是HTTP Only。

缺点:

不能处理表单。 所有AJAX post都必须包含头部。

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

用户登录成功。 服务器发出认证cookie。 用户单击以导航到表单。 页面在浏览器中加载,然后发出AJAX请求来检索CSRF令牌。 服务器生成CSRF令牌(如果尚未为会话生成),根据用户会话存储它并将其输出到 头。 用户通过AJAX提交表单(令牌通过报头发送)。 服务器检查自定义报头匹配会话存储令牌。

优点:

使用AJAX。 Cookie可以是HTTP Only。

缺点:

不能处理表单。 所有AJAX post也必须包含该值。 页面必须首先发出AJAX请求以获得CRSF令牌,因此这意味着每次都需要额外的往返。

set - cookie

用户登录成功。 服务器发出认证cookie。 用户单击以导航到表单。 服务器生成CSRF令牌,根据用户会话存储它并将其输出到cookie。 用户通过AJAX或HTML表单提交表单。 服务器检查自定义报头(或隐藏表单字段)匹配会话存储令牌。 Cookie可在浏览器中用于额外的AJAX和表单请求,无需向服务器发送额外的请求来检索CSRF令牌。

优点:

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

缺点:

所有表单都必须将值动态地添加到其HTML中。 任何AJAX post也必须包含该值。 cookie将被提交给每一个请求(即图像、CSS、JS等的所有get,不涉及到CSRF过程),以增加请求的大小。 Cookie不能为HTTP Only。

So the cookie approach is fairly dynamic offering an easy way to retrieve the cookie value (any HTTP request) and to use it (JS can add the value to any form automatically and it can be employed in AJAX requests either as a header or as a form value). Once the CSRF token has been received for the session, there is no need to regenerate it as an attacker employing a CSRF exploit has no method of retrieving this token. If a malicious user tries to read the user's CSRF token in any of the above methods then this will be prevented by the Same Origin Policy. If a malicious user tries to retrieve the CSRF token server side (e.g. via curl) then this token will not be associated to the same user account as the victim's auth session cookie will be missing from the request (it would be the attacker's - therefore it won't be associated server side with the victim's session).

As well as the Synchronizer Token Pattern there is also the Double Submit Cookie CSRF prevention method, which of course uses cookies to store a type of CSRF token. This is easier to implement as it does not require any server side state for the CSRF token. The CSRF token in fact could be the standard authentication cookie when using this method, and this value is submitted via cookies as usual with the request, but the value is also repeated in either a hidden field or header, of which an attacker cannot replicate as they cannot read the value in the first place. It would be recommended to choose another cookie however, other than the authentication cookie so that the authentication cookie can be secured by being marked HttpOnly. So this is another common reason why you'd find CSRF prevention using a cookie based method.

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

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

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

I think the 1st one, request body (while demonstrated by the Express tutorial I linked in the question), is not as portable to a wide variety of situations; not everyone is generating every HTTP response dynamically; where you end up needing to put the token in the generated response might vary widely (in a hidden form input; in a fragment of JS code or a variable accessible by other JS code; maybe even in a URL though that seems generally a bad place to put CSRF tokens). So while workable with some customization, #1 is a hard place to do a one-size-fits-all approach.

第二种方法是自定义header,它很有吸引力,但实际上不起作用,因为尽管JS可以获得它调用的XHR的header,但它无法获得它加载的页面的header。

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