【问题标题】:how to verify referrer inside a MVC or Web Api ajax call如何在 MVC 或 Web Api ajax 调用中验证引荐来源网址
【发布时间】:2015-02-11 23:24:44
【问题描述】:

我的 MVC 应用程序有常用的 ajax 方法(在 web api 和常规控制器中)。我想根据呼叫来自我的应用程序的哪个区域(视图)来授权这些呼叫。我面临的问题是如何验证 ajax 调用的来源。

我意识到这并不容易,因为 ajax 调用很容易被欺骗,但由于我可以完全控制视图的呈现方式(完整页面源代码),也许有一种方法可以嵌入防伪类型令牌,可以稍后被验证为 Url Referrer。

身份验证已经处理,我可以安全地验证调用的身份,唯一的问题是验证调用来自哪个 URL(MVC 路由)。更具体地说,防止用户能够欺骗 ajax 调用的来源。

我尝试创建一个自定义授权标头并在视图渲染和 ajax 调用之间传递它,这很有效,但仍然很容易被欺骗(因为用户可以从站点的另一部分嗅探标头并重新使用它们)。最后我不确定如何安全地验证标头没有被欺骗。唯一想到的是对令牌内的原始上下文的一些信息进行编码,并以某种方式针对传入调用上下文(在 ajax 调用中传递令牌的上下文)对其进行验证。

我看到 MVC 具有 AntiForgery 令牌功能,但我不确定这是否能解决我的问题。如果是这样,我想知道如何使用它来验证 /api/common/update 是从 /home/index/user/setup 调用的(这两个调用都是有效的)。

再次,我想要一种方法来验证 ajax 调用来自哪个页面,并且用户身份不是问题。

更新

按照@Sarathy 的建议,我尝试实施防伪令牌。据我所知,这是通过在每个页面上添加一个带有令牌的隐藏字段并将其与 cookie 中设置的令牌进行比较来实现的。这是我执行令牌验证的自定义操作过滤器属性的实现:

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var req = filterContext.RequestContext.HttpContext.Request;
        var fToken = req.Headers["X-Request-Verification-Token"];
        var cookie = req.Cookies[AntiForgeryConfig.CookieName];
        var cToken = cookie != null
            ? cookie.Value
            : "null";

        log.Info("filter \ntoken:{0} \ncookie:{1}", fToken, cToken);
        AntiForgery.Validate(cToken, fToken);
        base.OnActionExecuting(filterContext);
    }

那么我的防伪附加数据提供者如下所示:

public class MyAntiForgeryProvider : IAntiForgeryAdditionalDataProvider
{
    public string GetAdditionalData(System.Web.HttpContextBase context)
    {
        var ad  = string.Format("{0}-{1}",context.Request.Url, new Random().Next(9999));
        log.Info("antiforgery AntiForgeryProvider.GetAdditionalData Request.AdditionalData: {0}", ad);
        log.Info("antiforgery AntiForgeryProvider.GetAdditionalData Request.UrlReferrer: {0}", context.Request.UrlReferrer);
        return ad;
    }

    public bool ValidateAdditionalData(System.Web.HttpContextBase context, string additionalData)
    {
        log.Info("antiforgery AntiForgeryProvider.ValidateAdditionalData Request.Url: {0}", context.Request.Url);
        log.Info("antiforgery AntiForgeryProvider.ValidateAdditionalData additionalData: {0}", additionalData);
        return true;
    }

这很有效,因为我可以看到在提供程序中登录的正确页面,并且防伪在没有令牌的情况下中断。

但是,除非我做错了什么,否则这似乎是微不足道的恶搞。例如 如果我去pageA并复制令牌表单pageB(只是表单令牌,甚至不是cookie令牌),这仍然成功,并且在我的日志中我看到pageB同时从pageA执行ajax方法

确认这很容易被欺骗

我正在使用 csrf 生成这样的 ajax 令牌:

    public static string MyForgeryToken(this HtmlHelper htmlHelper)
    {
        var c = htmlHelper.ViewContext.RequestContext.HttpContext.Request.Cookies[AntiForgeryConfig.CookieName];
        string cookieToken, formToken;
        AntiForgery.GetTokens(c != null ? c.Value : null, out cookieToken, out formToken);
        return formToken;
    }

然后,我将表单令牌与每个 ajax 调用一起传回,并有一个自定义的 actionfilterattribute 与 cookie 令牌一起读取/验证它

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        var req = filterContext.RequestContext.HttpContext.Request;
        var fToken = req.Headers[GlobalConstants.AntiForgeKey];
        var cookie = req.Cookies[AntiForgeryConfig.CookieName];
        var cToken = cookie != null
            ? cookie.Value
            : "null";


        log.Info("MyAntiForgeryAttribute.OnActionExecuting. \ntoken:{0} \ncookie:{1}", fToken, cToken);
        AntiForgery.Validate(cToken, fToken);

这一切都有效(更改有关令牌的任何内容都会引发正确的异常),然后在我的 IAntiForgeryAdditionalDataProvider 中,我可以看到它认为它正在处理的内容。

一旦我从另一个视图覆盖 csrf 令牌,它就会认为它就是那个视图。我什至不必篡改 UrlReferrer 来打破这个:/

如果我可以强制 cookie 在每次页面加载时都不同,这可能会起作用

【问题讨论】:

  • 经典 XY 问题...
  • @DavidPeden 我已经概述了我的问题,并对如何以不同方式解决问题持开放态度。我还概述了我试图避免的其他可能的解决方案。也就是说,您的评论不是很有建设性
  • 很抱歉你有这种感觉。我发布这不是为了刻薄,而是让任何其他对阅读您的问题有足够兴趣的人有机会考虑您所问的不是您真正的意思(正如在@eol 的假设和您随后的 cmets 中明确讨论的那样)。我碰巧同意他的一般建议,即使你试图避免它。看起来你已经把自己画到了一个角落里,有一些我认为应该受到挑战的基本假设。如果这样做成本太高,我完全理解并且没有进一步的建议。
  • 好的,谢谢。是的,我完全有可能陷入死胡同,但这就是为什么我在这里发帖以验证我的方法中没有遗漏任何东西。这是一次非常有教育意义的经历;)
  • 正在重新考虑这一点,并有这样的想法:即使您可以验证 ajax 请求来自您的源页面,如何阻止恶意用户在该页面上实时编辑您的 JavaScript 以调用不同的api(或具有不同参数的相同api)?我认为您假设如果请求来自您的页面,则必须允许请求本身,因为您在该页面上编写了 JavaScript……但我认为这不是一个有效的假设。

标签: asp.net-mvc asp.net-mvc-4 asp.net-web-api csrf


【解决方案1】:

我假设您可以为此使用 IAntiForgeryAdditionalDataProvider。

public class CustomDataProvider : IAntiForgeryAdditionalDataProvider
{
   public string GetAdditionalData(HttpContextBase context)
   {
     // Return the current request url or build a route or create a hash from a set of items from the current context.
     return context.Request.Url.ToString();
    }

    public bool ValidateAdditionalData(HttpContextBase context, string additionalData)
    {
       // Check whether the allowed list contains additional data or delegate the validation to a separate component.
       return false;
     }
}

在 App_Start 中注册提供程序,如下所示。

AntiForgeryConfig.AdditionalDataProvider = new CustomDataProvider();

https://msdn.microsoft.com/en-us/library/system.web.helpers.iantiforgeryadditionaldataprovider(v=vs.111).aspx

希望这对您的方案有所帮助。

【讨论】:

  • 嘿,谢谢你的建议。我已经尝试过了,并将用我的结果添加我的问题的更新(太长,无法在此处列出)
  • 您是否尝试将附加数据与标题“Referer”进行比较?如果两者不同,则请求被欺骗。
  • 是的,但是引荐来源网址同样容易被欺骗......它只是一个 httpHeader。我目前的解决方案是使用自定义令牌 + 引荐来源哈希,所以它不那么明显,但一旦你知道它是如何工作的,仍然很容易欺骗
  • 是的,但是浏览器不允许任何人按照问题stackoverflow.com/questions/27218525/…中接受的答案设置“Referer”标头
  • hmm,如果引用者不是可欺骗的,那么我就不需要任何此类令牌业务。我只关心电话的来源。其实也不是不可能:en.wikipedia.org/wiki/Referer_spoofing#Tools
【解决方案2】:

您在问题中提到您正在寻找防伪令牌功能。

因此,我认为您要问的是反 CSRF 解决方案(CSRF=cross site request forgery)。

一种方法是在您的页面中呈现一个真正的随机数(一次性令牌),然后在每个请求中传递它,这可以通过添加键/值来完成与请求标头配对,然后在后端(即在您的控制器内部)进行检查。这是一种挑战-响应方法。

正如你所说,在服务器端代码中你可以使用

var fToken = req.Headers["X-Request-Verification-Token"];

从请求页面获取。

要从页面的每个客户端 AJAX 请求传递它,您可以使用

var tokenValue = '6427083747'; // replace this by rendered random token
$(document).ajaxSend(function (event, jqxhr, settings) {
        jqxhr.setRequestHeader('X-Request-Verification-Token', tokenValue);
});

或者您可以使用为每个请求设置它

var tokenValue = '2347893735'; // replace this by rendered random token
$.ajax({
    url: 'foo/bar',
    headers: { 'X-Request-Verification-Token': tokenValue }
});

注意tokenValue需要包含网页发送给客户端时网页服务器渲染的随机数。

我不会为此使用 cookie,因为 cookie 不能保护您免受 CSRF 的影响——您需要确保请求的页面与呈现的页面相同(因此由 Web 服务器创建) .位于同一浏览器窗口中不同选项卡上的页面也可以使用 cookie。

详情可在OWASP project 页面上的OWASP CSRF prevention cheat sheet 中找到。

【讨论】:

  • 我也在考虑类似的事情。此外,后端需要跟踪它最后一次为该会话和 url 组合提供的一次性令牌(这可能存储在临时会话表中)。当 AJAX 请求发生时,后端会验证 URL、会话和令牌是否都匹配。因此,令牌对于任何不同的 URL(试图通过欺骗请求更改不同的模型)和任何不同的会话都是无效的。如果你真的想搞砸,你也可以在请求回调中返回一个新的token,让它一次性使用。
  • @Aaron D:你是对的,在服务器端,你需要跟踪会话对象中的令牌,只要用户登录,它就会“存活”。每次更改令牌时间难以实施。
  • 嗨,马特,感谢您的详细帖子。我已经实现了这一点,完成了跟踪服务器和管理页面上的令牌缓存,让我看到/清除令牌。我还更进一步将 url 哈希“salt”添加到令牌中,然后将其与 urlReferrer 进行比较。这有点帮助,但它仍然很容易规避..确实需要一点知识或尝试和失败,在此期间我希望能够发现错误..
  • 我所说的“易于规避”的意思是最终该令牌用于建立访问级别,但是可以从其他视图复制和使用该令牌,因为http头很容易覆盖.几乎准备用这种方法认输。我正在研究 httpOnly cookie owasp.org/index.php/HttpOnly
  • @Sonic Soul:不客气。当然,您应该将此与服务器上的 Windows 身份验证+授权结合起来,而不是单独依赖令牌。是的,传递令牌的另一种方式是 httpOnly,正如描述的那样,它是实现此目的的不同方式。
【解决方案3】:

我的快速临时解决方案是使用在每个页面加载时创建的自定义令牌(我在令牌缓存中跟踪的 guid),这些令牌在所有 ajax 调用中作为标头传递。此外,我创建了一个原始 url 哈希并将其组合到自定义身份验证令牌中。 然后在我的 ajax 方法中提取哈希并将其与 UrlReferrer 哈希进行比较,以确保它没有被篡改。 由于自定义令牌总是不同的,因此猜测发生了什么不太明显,因为令牌在每次页面加载时似乎都不同。然而,这并不安全,因为只要付出足够的努力,就可以发现 url 哈希。由于用户身份不是问题,因此暴露在一定程度上是有限的,因此最坏的情况是给定用户将获得对站点另一部分的写访问权限,但只能以他自己的身份访问。我的网站是内部的,我正在审核一举一动,因此任何发脾气的企图都会很快被抓住。

我同时使用 jQuery 和 Angular,因此在所有请求中附加标记,如下所示:

var __key = '@Html.GetHeaderKey()' //helper method to get key from http header
//jQuery
$.ajaxSetup({
    beforeSend: function (xhr, settings) {
    xhr.setRequestHeader('X-Nothing-To-See-Here', __key); // totally inconspicuous 
})

//angular
app.config(['$httpProvider', function ($httpProvider) {
  $httpProvider.defaults.headers.common['X-Nothing-To-See-Here'] = __key;
});

更新

这种方法的缺点是自定义令牌需要在网络场或应用重新启动时保留。基于@Sarathy 的想法,我试图通过利用 MVC 防伪框架来解决这个问题。基本上添加/删除我的“盐”并让框架管理实际的令牌验证。这样对我来说管理起来就少了一点。一旦我确认这是有效的,将发布更多详细信息。

【讨论】:

  • 我很好奇需求的来源。你想阻止什么行为?
  • 我的授权模型基于每个视图(MVC 路径),并且我有从站点内不同位置调用的通用 ajax 方法。我想根据调用的来源视图授权他们。
  • 很抱歉,我没有提供更多帮助。您是否有理由不在 api 控制器上使用与 mvc 控制器上相同的授权模型。
  • np!实际上我使用的是相同的模型。但这只是让我对 web api 方法进行全有或全无访问。所以我可以说用户 A 可以或不能访问 /api/entity/update 方法。但它不允许我根据呼叫来自站点的位置来声明访问权限。以安全的方式拥有该功能会很好。
  • 另一种选择是表级或行级访问,但如果您有 100 个表,则需要大量维护,因此只为少数几个表保留。
【解决方案4】:

所以这将是我不喜欢的“你做错了”的答案之一,所以我先道歉。无论如何,从问题和 cmets 来看,我将建议您以不同的方式解决问题。与其考虑请求来自何处,不如考虑请求试图做什么。您需要确定用户是否可以这样做。

我猜为什么这对你来说很难,我认为你的 api 接口太通用了。从您的示例 api“api/common/update”中,我猜您有一个可以更新任何内容的通用更新 api,并且您希望保护从只应该访问数据 Y 的页面更新数据 X。如果我离开基地然后不理我。 :)

所以我的回答是:不要那样做。更改您的 api,使其从您要使用的数据开始:api/dataX api/dataY。然后使用用户角色适当地保护这些 api 方法。在幕后,您仍然可以有一个通用的更新例程,如果您喜欢它并且它对您有用,但请保持 api 接口更加具体。

如果您真的不想为每个表都有一个 api,并且如果它适合您的情况,也许您至少可以有一个用于受保护/管理表的 api 和一个用于标准表的单独 api。很多“如果”,但也许这对你的情况有用。

此外,如果您的用户可以更新某些 dataX 但不能更新其他 dataX,那么您将必须对您的数据进行某种检查,最好是针对某个根对象以及您的用户是否有权查看/使用该根对象.

总而言之,避免使用过于通用的 api 接口。通过更具体,您可以使用现有的安全工具来帮助您。

祝你好运!

【讨论】:

  • 你的假设是对的,但是这种方法是我试图避免的。我已经有单独的管理员控制器等,所以没问题。但是我确实有一个通用的更新方法,我希望保留它,因为就像我说的那样,我的模式有 100 个表,所以在那之后对控制器建模将是一个巨大的痛苦。在一些表中,我实现了行级授权,可以安全地绕过控制器级身份验证,但我不想再对 100 个表执行此操作,因为管理起来将是一场噩梦
  • 基本上我正在寻找一个明确的答案,以确定我正在尝试做的事情是否可能。似乎这是不可能的,但我不能百分百自信地这么说
猜你喜欢
  • 2010-12-01
  • 1970-01-01
  • 2020-08-04
  • 2016-10-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-15
  • 2017-10-31
相关资源
最近更新 更多