使用WebApi时,防伪cookie令牌和表单字段令牌不匹配

9

我有一个单页应用程序(用户加载一堆HTML/JS,然后通过WebAPI进行AJAX请求,而不需要另一个MVC调用)。在WebAPI中我有以下内容:

public sealed class WebApiValidateAntiForgeryTokenAttribute : ActionFilterAttribute
{
    public override void OnActionExecuting(
        System.Web.Http.Controllers.HttpActionContext actionContext)
    {
        if (actionContext == null)
        {
            throw new ArgumentNullException(nameof(actionContext));
        }
        if (actionContext.Request.Method.Method == "POST")
        {
            string requestUri = actionContext.Request.RequestUri.AbsoluteUri.ToLower();
            if (uriExclusions.All(s => !requestUri.Contains(s, StringComparison.OrdinalIgnoreCase))) // place some exclusions here if needed
            {
                HttpRequestHeaders headers = actionContext.Request.Headers;

                CookieState tokenCookie = headers
                    .GetCookies()
                    .Select(c => c[AntiForgeryConfig.CookieName]) // __RequestVerificationToken
                    .FirstOrDefault();

                string tokenHeader = string.Empty;
                if (headers.Contains("X-XSRF-Token"))
                {
                    tokenHeader = headers.GetValues("X-XSRF-Token").FirstOrDefault();
                }

                AntiForgery.Validate(!string.IsNullOrEmpty(tokenCookie?.Value) ? tokenCookie.Value : null, tokenHeader);
            }

        }
        base.OnActionExecuting(actionContext); // this is where it throws
    }
}

在 Global.asax 中注册:

    private static void RegisterWebApiFilters(HttpFilterCollection filters)
    {
        filters.Add(new WebApiValidateAntiForgeryTokenAttribute());
        filters.Add(new AddCustomHeaderFilter());
    }

我偶尔会在日志中看到“防伪cookie令牌和表单字段令牌不匹配”的错误。当发生这种情况时,tokenCookie.valuetokenHeader 都不为null。

在客户端,我所有的AJAX请求都使用以下内容:

beforeSend: function (request) {
     request.setRequestHeader("X-XSRF-Token", $('input[name="__RequestVerificationToken"]').attr("value"););
},

使用 Razor 在我的 SPA 页面上生成令牌:
@Html.AntiForgeryToken()

我已经在Web.config中设置了我的机器密钥。

可能是什么原因导致这个问题?

更新 我刚刚检查了日志,有时会看到这个:

提供的防伪令牌是为用户“”,但当前用户是“someuser@domain.com”。 几秒钟前

当用户在登录的情况下刷新他们的SPA实例时就会发生这种情况。 SPA将他们降落到着陆页而不是内部页面,原因不明(User.Identity.IsAuthenticated 为true) - 然后他们无法登录,因为出现这个错误。 刷新后他们又回到了SPA内部。 不确定这意味着什么,但我认为提供更多信息并不会有坏处。

附录 https://security.stackexchange.com/questions/167064-is-csrf-protection-useless-with-ajax/167076#167076


1
防伪令牌在 cookie 上使用超时,这可能是原因。 - Tasos K.
@TasosK。我一直在监控这个问题,似乎与超时无关 - 我已经成功验证了几天后的令牌,并在几秒或几分钟后看到了失败。 - SB2055
@wal 我确实在 5.2.3 上。 - SB2055
1
@SB2055 这可能有些困难,但是尝试在您使用的登录方法上添加 {[OutputCache(NoStore=true, Duration = 0, VaryByParam= "None")]} 属性。 - MarkovskI
你说你偶尔看到它...可能是AF令牌正在发挥作用,保护你免受被攻击的会话吗?或者,你的会话超时是否在每个API调用上延长,或者只是其中一些导致间歇性失败? - CResults
显示剩余12条评论
2个回答

4
我的建议是不要尝试在AJAX调用中使用基于令牌的CSRF保护,而是依赖于Web浏览器的本地CORS功能。
基本上,从浏览器到后端服务器的任何AJAX调用都会检查域源(也就是脚本加载的域)。如果域名匹配(JS托管域==目标AJAX服务器域),则AJAX调用正常进行,否则返回null
如果攻击者试图在自己的服务器上托管恶意AJAX查询,则如果您的后端服务器没有允许他这样做的CORS策略(默认情况下),它将失败。
因此,在AJAX调用中,原生的CSRF保护毫无用处,你可以通过简单地不尝试处理它来降低技术债务
更多信息,请参见CORS - Mozilla Foundation 代码示例-使用控制台检查器!

<html>
<script>
function reqListener () {
  console.log(this.responseText);
}

var oReq = new XMLHttpRequest();
oReq.addEventListener("load", reqListener);
oReq.open("GET", "http://www.reuters.com/");
oReq.send();
</script>
</html>

运行程序并查看安全错误:

跨域请求被阻止:同源策略禁止读取远程资源 http://www.reuters.com/。(原因:CORS头文件‘Access-Control-Allow-Origin’缺失)。

Mozilla对于跨站点XMLHttpRequest实现非常明确:

现代浏览器通过实现Web应用程序(WebApps)工作组的跨站点请求访问控制标准来支持跨站点请求。

只要服务器配置为允许来自您的Web应用程序来源的请求,XMLHttpRequest就会工作。否则,将抛出INVALID_ACCESS_ERR异常。


哦,哇——我不知道那个。当解决方案是删除东西时,我很喜欢。 - SB2055
如果那个解决方案对您有帮助,请毫不犹豫地验证一下;-) - Fabien
我现在正在进一步研究 - 看起来标题可能是必需的,但值不需要验证?我在这里看到了一些相互矛盾的指导:https://security.stackexchange.com/questions/167064/is-csrf-protection-useless-with-ajax/167076#167076 - SB2055
“指南”中说:“您可以使用基本的JavaScript XMLHttpRequest对象进行GET或POST请求。由于可以使用HTML表单从跨站点执行等效请求,因此您很容易受到攻击。”这是绝对错误的。我将更新我的答案并包含一个代码片段,以便您自己查看。 - Fabien
更新完成。你可以信任ASP.NET开发人员,但没有人说你应该;-D - Fabien
我为你的回答点赞,因为其中包含有用的信息,但它并没有解释为什么会像OP中描述的那样发生。 - Ciro Corvino

0

我试图给出一个相同的答案,即使在评论中我们交流,你的情况似乎与我的不相关。

这种类型的问题可能是由于XMLHttpRequest.setRequestHeader()的行为引起的,因为该函数“组合”已经在http请求的上下文中分配了一个标头的值,如MDNWhatwg所述:

如果多次使用相同的标头调用此方法,则 值将合并为单个请求标头。

因此,如果我们有一个例如执行所有ajax POSTs设置给定http标头的SPA,在您的情况下:

beforeSend: function (request) {
     request.setRequestHeader("X-XSRF-Token", $('input[name="__RequestVerificationToken"]').attr("value"););
}

第一个ajax POST 请求设置了一个清晰的头信息("X-XSRF-Token"),因此,在服务器端,您应该有一个“有效”的头信息值进行比较。

但是,在没有页面刷新或新的GET请求的情况下,所有后续的ajax POST请求都会进行脏分配相同的头信息("X-XSRF-Token"),因为它们将新值与旧值合并在一起,正如在MDNWhatwg文档中所述。

为了避免这个问题,您可以尝试重置"X-XSRF-Token"的值(但对此并没有太多的文档资料,这似乎不是一个可靠的解决方案...)

beforeSend: function (request) {
     request.setRequestHeader("X-XSRF-Token", null);      //depends on user agents..
     //OR.. request.setRequestHeader("X-XSRF-Token", ''); //other user agents..
     //OR.. request.setRequestHeader("X-XSRF-Token");     //other user agents..
     request.setRequestHeader("X-XSRF-Token", $('input[name="__RequestVerificationToken"]').attr("value"););
}

其他解决方案可能会依赖于某些客户端状态处理机制,您必须自己实现,因为不可能获取http请求头的值或状态访问(只能访问响应头)。

更新 - 以下文本进行修订: 因此,如果我们有一个例如执行所有ajax POST的SPA,使用每次调用重新使用XMLHttpRequest对象并设置给定的http头,对于您的情况: ...


这不可能是全部。每当您创建一个新的XMLHttpRequest对象时,它都有一个空的头列表与之关联。 - Anne
从OP的内容中并不清楚XMLHttpRequest是每次还是只实例化一次。 - Ciro Corvino
每次都是新的请求,所以我不认为这就是答案 :/ - SB2055
好的。但是尝试在函数体中添加console.log,以查看它是否被调用了多次,并且在扩展过滤器onActionExecuting中检查从头部令牌返回了什么。 - Ciro Corvino

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接