Web API和ValidateAntiForgeryToken


73

我们有一些现有的MVC Web服务,这些Web服务在网页中称为AJAX样式。这些服务利用ValidateAntiForgeryToken属性来帮助防止请求伪造。

我们正在寻求将这些服务迁移到Web API,但似乎没有等效的防伪功能。

我想念什么吗?是否存在使用Web API解决请求伪造的其他方法?


3
尽管达林的答案是正确的,但达兹·威尔金(DazWilkin)引导我们成为将令牌放入标头的更好解决方案。 stackoverflow.com/questions/11725988/...
斯科特

8
更好的解决方案也来自达林(Darin):)
彼得·波菲

Answers:


56

您可以实现以下授权属性:

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public sealed class ValidateAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter
{
    public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
    {
        try
        {
            AntiForgery.Validate();
        }
        catch
        {
            actionContext.Response = new HttpResponseMessage 
            { 
                StatusCode = HttpStatusCode.Forbidden, 
                RequestMessage = actionContext.ControllerContext.Request 
            };
            return FromResult(actionContext.Response);
        }
        return continuation();
    }

    private Task<HttpResponseMessage> FromResult(HttpResponseMessage result)
    {
        var source = new TaskCompletionSource<HttpResponseMessage>();
        source.SetResult(result);
        return source.Task;
    }
}

然后用它装饰您的API操作:

[ValidateAntiForgeryToken]
public HttpResponseMessage Post()
{
    // some work
    return Request.CreateResponse(HttpStatusCode.Accepted);
}

谢谢,System.Web.Helpers.AntiForgery看起来将是我的问题的答案。
ScottS

就我而言,数据通过以下代码作为JSON字符串出现,并且上述解决方案无效:$ .ajax({url:url,method:“ PUT”,contentType:“ application / json”,dataType:“ json“,数据:formJsonData})
Tohid

3
我认为这种方法只有在您使用表单编码发布到ajax服务并将其显式包含在ajax请求表单数据中时才有效。如果要对ajax数据使用JSON,则需要滚动代码以提取令牌并调用AntiForgery.Validate的两参数重载。您可以在json数据中或在HTTP标头中发送“ formToken”参数,例如stephenwalther.com/archive/2013/03/05/…–
Andy

2
@Darin我是对的,如果没有引用就不可能拥有AntiForgerySystem.Web.WebPages吗?因为我真的不想引用我的WebAPI库WebPages...
Alex Zhukovskiy

1
@RuudLendersAntiForgery位于中Microsoft.AspNet.WebPages,这显然取决于WebPagesSystem.Web.Razor。但是,无法使用API​​进行此操作,因为在这种情况下,由于没有页面,因此无法在页面上注入防伪令牌。
亚历克斯·朱可夫斯基

20

补充上面的代码FilterAttribute

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
    public sealed class ValidateAntiForgeryTokenAttribute : FilterAttribute, IAuthorizationFilter
    {
        public Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(HttpActionContext actionContext, CancellationToken cancellationToken, Func<Task<HttpResponseMessage>> continuation)
        {
            try
            {
                string cookieToken = "";
                string formToken = "";

                IEnumerable<string> tokenHeaders;
                if (actionContext.Request.Headers.TryGetValues("RequestVerificationToken", out tokenHeaders))
                {
                    string[] tokens = tokenHeaders.First().Split(':');
                    if (tokens.Length == 2)
                    {
                        cookieToken = tokens[0].Trim();
                        formToken = tokens[1].Trim();
                    }
                }
                AntiForgery.Validate(cookieToken, formToken);
            }
            catch (System.Web.Mvc.HttpAntiForgeryException e)
            {
                actionContext.Response = new HttpResponseMessage
                {
                    StatusCode = HttpStatusCode.Forbidden,
                    RequestMessage = actionContext.ControllerContext.Request
                };
                return FromResult(actionContext.Response);
            }
            return continuation();
        }

        private Task<HttpResponseMessage> FromResult(HttpResponseMessage result)
        {
            var source = new TaskCompletionSource<HttpResponseMessage>();
            source.SetResult(result);
            return source.Task;
        }

使用剃刀的HTML功能

@functions{
    public string TokenHeaderValue()
        {
            string cookieToken, formToken;
            AntiForgery.GetTokens(null, out cookieToken, out formToken);
            return cookieToken + ":" + formToken;
        }
}

使用角度

return $http({
   method: 'POST',
   url: '@Url.Content("~/api/invite/")',
   data: {},
   headers: {
       'RequestVerificationToken': '@TokenHeaderValue()'
   }
});

8
您至少应该提供给与者参考。原始文章在这里 asp.net/web-api/overview/security/…–
Idrees Khan

为什么这比答案更好?
伊恩·沃伯顿

6
我不认为这可行(复制的原件也不可行)。cookieToken必须作为httpOnly cookie发送,以便客户端脚本无法对其进行操作。通过将两个令牌简单地连接在一起就可以构成一个令牌,这意味着JavaScript可以同时操纵这两个令牌,因此您将击败该机制的整个对象。
安迪

1
嗨,@Oswaldo,你能不能,请解释为什么ValidateAntiForgeryTokenAttribute工具IAuthorizationFilter,而不是IAuthenticationFilter
杰拉尔多·利马

@Andy问题是谁的JavaScript?此方法依赖于第三方恶意网站的javascript无法访问cookie,而受保护的网站的javascript可以访问cookie。受保护网站页面上的XSS漏洞将通过允许攻击者获得对Cookie的访问来解决此问题。因此,防范XSS是此方法的关键。
约翰

6

链接帮助您从剃刀视图中检索防伪令牌并将该令牌作为标头传递:

var csrfToken = $("input[name='__RequestVerificationToken']").val(); 
$.ajax({
    headers: { __RequestVerificationToken: csrfToken },
    type: "POST",
    dataType: "json",
    contentType: 'application/json; charset=utf-8',
    url: "/api/products",
    data: JSON.stringify({ name: "Milk", price: 2.33 }),
    statusCode: {
        200: function () {
            alert("Success!");
        }
    }
});

1
“ ValidateAjaxAntiForgeryToken仅在对用户进行身份验证时才起作用,而对匿名请求则不起作用。”
Rudey

@Rudey-嗯。你确定吗?
curiousBoy

是的,那是答案中链接文章的直接引文。
Rudey

5

奥斯瓦尔多的答案,但被实现为AuthorizeAttribute

  [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
  public class ApiValidateAntiForgeryToken : AuthorizeAttribute
  {
    public static string GenerateAntiForgeryTokenForHeader() {
      string cookieToken, formToken;
      AntiForgery.GetTokens(null, out cookieToken, out formToken);
      return cookieToken + ":" + formToken;
    }


    protected override bool IsAuthorized(HttpActionContext actionContext) {
      var headers = actionContext.Request.Headers;

      // we pass both the cookie and the form token into a single header field
      string headerToken = headers.Contains("__RequestVerificationToken") ? headers.GetValues("__RequestVerificationToken").FirstOrDefault() : null;

      if (headerToken == null) {
        return false;
      }

      string[] tokens = headerToken.Split(':');
      if (tokens.Length != 2) {
        return false;
      }

      string cookieToken = tokens[0].Trim();
      string formToken = tokens[1].Trim();

      try {
        AntiForgery.Validate(cookieToken, formToken);
      }
      catch {
        return false;
      }

      return base.IsAuthorized(actionContext);
    }
  }

您可以使用[ApiValidateAntiForgeryToken]装饰控制器或方法,然后将RequestVerificationToken:“ @ ApiValidateAntiForgeryToken.GenerateAntiForgeryTokenForHeader()”作为剃刀javascript代码中的方法标题。


嗨,@ Javier,能否请您解释一下为什么您ApiValidateAntiForgeryToken 继承自AuthorizeAttribute实现而不是实现IAuthorizationFilter
杰拉尔多·利马

1
嗨,Gerardo,基本上这是相同的事情,但是实现方式不同。请参阅stackoverflow.com/questions/27021506/…-在我的情况下,我需要一个需要AutorizeAttribute并且不能使用该接口的项目(我认为这是因为SignalR,但我已经不记得了)。无论如何,请考虑我在同一篇文章中给出的其他答案,因为它更安全。
哈维尔·

5

再三考虑之后,将cookie和表单令牌混合在一起是一个坏主意,因为它破坏了反伪造令牌的整个目的。最好将cookie部分保留为cookie,同时将表单部分移动到auth标头,因此最好使用新的答案(再次作为AuthorizeAttribute)。

using System;
using System.Linq;
using System.Net.Http;
using System.Web;
using System.Web.Helpers;
using System.Web.Http;
using System.Web.Http.Controllers;

  [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)]
  public class ApiValidateAntiForgeryToken : AuthorizeAttribute {
    public const string HeaderName = "X-RequestVerificationToken";

    private static string CookieName => AntiForgeryConfig.CookieName;

    public static string GenerateAntiForgeryTokenForHeader(HttpContext httpContext) {
      if (httpContext == null) {
        throw new ArgumentNullException(nameof(httpContext));
      }

      // check that if the cookie is set to require ssl then we must be using it
      if (AntiForgeryConfig.RequireSsl && !httpContext.Request.IsSecureConnection) {
        throw new InvalidOperationException("Cannot generate an Anti Forgery Token for a non secure context");
      }

      // try to find the old cookie token
      string oldCookieToken = null;
      try {
        var token = httpContext.Request.Cookies[CookieName];
        if (!string.IsNullOrEmpty(token?.Value)) {
          oldCookieToken = token.Value;
        }
      }
      catch {
        // do nothing
      }

      string cookieToken, formToken;
      AntiForgery.GetTokens(oldCookieToken, out cookieToken, out formToken);

      // set the cookie on the response if we got a new one
      if (cookieToken != null) {
        var cookie = new HttpCookie(CookieName, cookieToken) {
          HttpOnly = true,
        };
        // note: don't set it directly since the default value is automatically populated from the <httpCookies> config element
        if (AntiForgeryConfig.RequireSsl) {
          cookie.Secure = AntiForgeryConfig.RequireSsl;
        }
        httpContext.Response.Cookies.Set(cookie);
      }

      return formToken;
    }


    protected override bool IsAuthorized(HttpActionContext actionContext) {
      if (HttpContext.Current == null) {
        // we need a context to be able to use AntiForgery
        return false;
      }

      var headers = actionContext.Request.Headers;
      var cookies = headers.GetCookies();

      // check that if the cookie is set to require ssl then we must honor it
      if (AntiForgeryConfig.RequireSsl && !HttpContext.Current.Request.IsSecureConnection) {
        return false;
      }

      try {
        string cookieToken = cookies.Select(c => c[CookieName]).FirstOrDefault()?.Value?.Trim(); // this throws if the cookie does not exist
        string formToken = headers.GetValues(HeaderName).FirstOrDefault()?.Trim();

        if (string.IsNullOrEmpty(cookieToken) || string.IsNullOrEmpty(formToken)) {
          return false;
        }

        AntiForgery.Validate(cookieToken, formToken);
        return base.IsAuthorized(actionContext);
      }
      catch {
        return false;
      }
    }
  }

然后只需使用[ApiValidateAntiForgeryToken]装饰您的控制器或方法

并将其添加到razor文件中以为javascript生成令牌:

<script>
var antiForgeryToken = '@ApiValidateAntiForgeryToken.GenerateAntiForgeryTokenForHeader(HttpContext.Current)';
// your code here that uses such token, basically setting it as a 'X-RequestVerificationToken' header for any AJAX calls
</script>
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.