前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C# AntiForgeryToken防XSRF漏洞攻击

C# AntiForgeryToken防XSRF漏洞攻击

作者头像
全栈程序员站长
发布2022-08-31 15:40:23
1.3K0
发布2022-08-31 15:40:23
举报

大家好,又见面了,我是你们的朋友全栈君。

1.XSRF:跨站请求伪造

XSRF即在访问B站点的时候,执行了A站点的功能。 比如: A站点登录后,可以修改用户的邮箱(接口:/Email/Modify?email=123),修改邮箱时只验证用户有没有登录,而且登录信息是保存在cookie中。 用户登录A站点后,又打开一个窗口访问B站点,如果这时B站点内嵌入了一条链接http://www.A.com/Email/Modify?email=123,当用户点击这条链接时会直接修改A站点的用户邮箱。

2.ASP.NET 防XSRF攻击

ASP.NET提供了AntiForgery类防止XSRF攻击。 AntiForgery的使用如下: 在ASP.NET页面中添加如下代码

代码语言:javascript
复制
@Html.AntiForgeryToken()

在Controller的Action上添加属性ValidateAntiForgeryToken

代码语言:javascript
复制
[ValidateAntiForgeryToken]
public ActionResult IndexPost()
{
      return View("~/Views/Home/Index.cshtml");
 }

这样IndexPost就能防止XSRF攻击。

3.AntiForgery防XSRF攻击原理

在执行@Html.AntiForgeryToken()语句时,会在cookie中写入一个经过加密后的数据,并在页面中添加一个隐藏域一并写入加密后的数据(默认名称为__RequestVerificationToken)。当执行IndexPost(前面示例)方法前,会判断cookie中的数据与隐藏域的数据是否相等。相等则验证通过。否则会抛出异常。(Post请求会自动把隐藏域传递到后台,如果是Get请求,就需要手动把隐藏域的值传递到后台)。 待加密的数据是一个AntiForgeryToken对象。系统进行验证时,会先把加密的数据还原成AntiForgeryToken对象,对象有一个SecurityToken属性(用于填充随机序列),系统主要判断该字段的值是否相等。 同一个会话期间,SecurityToken数据相同,所以即使开多个tab访问相同页面,数据验证也会通过。 同一个会话期间cookie中的加密数据不会改变,因为访问页面时,cookie会传到后台,后台判断cookie中有加密数据,就不会重新生成cookie数据。但隐藏域的值每次都不同,因为每访问一次页面,都会重新加密一次,虽然AntiForgeryToken对象的值相同,但通过MachineKey的Protect加密后,每次加密的值都会不同。 AntiForgery使用MachineKey进行加密,所以如果系统使用负载均衡,就需要配置MachineKey,否则不同服务器的MachineKey不同,导致无法解密。

4.源码解析

1)在执行@Html.AntiForgeryToken()语句时,会调用GetHtml方法。GetHtml方法中会调用GetFormInputElement方法,该方法会在cookie中写入加密后的数据,并返回Html标签代码。该标签代码会写入到页面中。

代码语言:javascript
复制
        public static HtmlString GetHtml()
        {
            if (HttpContext.Current == null)
            {
                throw new ArgumentException(WebPageResources.HttpContextUnavailable);
            }

            TagBuilder retVal = _worker.GetFormInputElement(new HttpContextWrapper(HttpContext.Current));
            return retVal.ToHtmlString(TagRenderMode.SelfClosing);
        }

2)在GetFormInputElement方法中,首先通过GetCookieTokenNoThrow方法获取Cookie中AntiForgeryToken对象(第一访问页面该对象为空)。再通过GetTokens方法获取新的newCookieToken以及formToken(newCookieToken就是写入cookie的token,formToken就是写入隐藏域的token)。如果oldCookieToken不为空,那么newCookieToken就会为空,这样就不会重新写入cookie。所以同一个会话期间cookie值会相同。如果不为空就通过SaveCookieToken方法写入cookie。

代码语言:javascript
复制
        public TagBuilder GetFormInputElement(HttpContextBase httpContext)
        {
            CheckSSLConfig(httpContext);

            AntiForgeryToken oldCookieToken = GetCookieTokenNoThrow(httpContext);
            AntiForgeryToken newCookieToken, formToken;
            GetTokens(httpContext, oldCookieToken, out newCookieToken, out formToken);

            if (newCookieToken != null)
            {
                // If a new cookie was generated, persist it.
                _tokenStore.SaveCookieToken(httpContext, newCookieToken);
            }

            if (!_config.SuppressXFrameOptionsHeader)
            {
                // Adding X-Frame-Options header to prevent ClickJacking. See
                // http://tools.ietf.org/html/draft-ietf-websec-x-frame-options-10
                // for more information.
                httpContext.Response.AddHeader("X-Frame-Options", "SAMEORIGIN");
            }

            // <input type="hidden" name="__AntiForgeryToken" value="..." />
            TagBuilder retVal = new TagBuilder("input");
            retVal.Attributes["type"] = "hidden";
            retVal.Attributes["name"] = _config.FormFieldName;
            retVal.Attributes["value"] = _serializer.Serialize(formToken);
            return retVal;
        }

3)SaveCookieToken方法先通过Serialize方法序列化,序列化的时候会对数据加密,再写入cookie。

代码语言:javascript
复制
        public void SaveCookieToken(HttpContextBase httpContext, AntiForgeryToken token)
        {
            string serializedToken = _serializer.Serialize(token);
            HttpCookie newCookie = new HttpCookie(_config.CookieName, serializedToken)
            {
                HttpOnly = true
            };

            // Note: don't use "newCookie.Secure = _config.RequireSSL;" since the default
            // value of newCookie.Secure is automatically populated from the <httpCookies>
            // config element.
            if (_config.RequireSSL)
            {
                newCookie.Secure = true;
            }

            httpContext.Response.Cookies.Set(newCookie);
        }

4)GetTokens方法,如果oldCookieToken不为空,就不重新生成newCookieToken。为空则通过GenerateCookieToken方法生成一个Token。再调用GenerateFormToken方法生成formToken。

代码语言:javascript
复制
        private void GetTokens(HttpContextBase httpContext, AntiForgeryToken oldCookieToken, out AntiForgeryToken newCookieToken, out AntiForgeryToken formToken)
        {
            newCookieToken = null;
            if (!_validator.IsCookieTokenValid(oldCookieToken))
            {
                // Need to make sure we're always operating with a good cookie token.
                oldCookieToken = newCookieToken = _validator.GenerateCookieToken();
            }

            Contract.Assert(_validator.IsCookieTokenValid(oldCookieToken));
            formToken = _validator.GenerateFormToken(httpContext, ExtractIdentity(httpContext), oldCookieToken);
        }

5)GenerateCookieToken方法生成cookieToken,即创建一个新的AntiForgeryToken对象。AntiForgeryToken有个SecurityToken属性,类型为BinaryBlob。BianryBlob对象会通过RNGCryptoServiceProvider实例的GetBytes方法填充强随机序列。填充的序列就是用来验证的随机数。即随机数是在创建AntiForgeryToken对象时自动生成的。

代码语言:javascript
复制
        public AntiForgeryToken GenerateCookieToken()
        {
            return new AntiForgeryToken()
            {
                // SecurityToken will be populated automatically.
                IsSessionToken = true
            };
        }

6)GenerateFormToken方法,就是把cookieToken的SecurityToken赋值给formToken。这样就会使得cookieToken与formToken的SecurityToken值相等。

代码语言:javascript
复制
        public AntiForgeryToken GenerateFormToken(HttpContextBase httpContext, IIdentity identity, AntiForgeryToken cookieToken)
        {
            Contract.Assert(IsCookieTokenValid(cookieToken));

            AntiForgeryToken formToken = new AntiForgeryToken()
            {
                SecurityToken = cookieToken.SecurityToken,
                IsSessionToken = false
            };

            bool requireAuthenticatedUserHeuristicChecks = false;
            // populate Username and ClaimUid
            if (identity != null && identity.IsAuthenticated)
            {
                if (!_config.SuppressIdentityHeuristicChecks)
                {
                    // If the user is authenticated and heuristic checks are not suppressed,
                    // then Username, ClaimUid, or AdditionalData must be set.
                    requireAuthenticatedUserHeuristicChecks = true;
                }

                formToken.ClaimUid = _claimUidExtractor.ExtractClaimUid(identity);
                if (formToken.ClaimUid == null)
                {
                    formToken.Username = identity.Name;
                }
            }

            // populate AdditionalData
            if (_config.AdditionalDataProvider != null)
            {
                formToken.AdditionalData = _config.AdditionalDataProvider.GetAdditionalData(httpContext);
            }

            if (requireAuthenticatedUserHeuristicChecks
                && String.IsNullOrEmpty(formToken.Username)
                && formToken.ClaimUid == null
                && String.IsNullOrEmpty(formToken.AdditionalData))
            {
                // Application says user is authenticated, but we have no identifier for the user.
                throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture,
                    WebPageResources.TokenValidator_AuthenticatedUserWithoutUsername, identity.GetType()));
            }

            return formToken;
        }

7)生成cookieToken和formToken后就会调用Serialize方法进行序列化。序列化的时候会调用MachineKey的Protect方法进行加密。每次加密后的值都不相同。如果使用了负载均衡,一定要配置MachineKey,而不能使用系统的值。

代码语言:javascript
复制
 public string Serialize(AntiForgeryToken token)
        {
            Contract.Assert(token != null);

            using (MemoryStream stream = new MemoryStream())
            {
                using (BinaryWriter writer = new BinaryWriter(stream))
                {
                    writer.Write(TokenVersion);
                    writer.Write(token.SecurityToken.GetData());
                    writer.Write(token.IsSessionToken);

                    if (!token.IsSessionToken)
                    {
                        if (token.ClaimUid != null)
                        {
                            writer.Write(true /* isClaimsBased */);
                            writer.Write(token.ClaimUid.GetData());
                        }
                        else
                        {
                            writer.Write(false /* isClaimsBased */);
                            writer.Write(token.Username);
                        }

                        writer.Write(token.AdditionalData);
                    }

                    writer.Flush();
                    return _cryptoSystem.Protect(stream.ToArray());
                }
            }
        }

发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/144347.html原文链接:https://javaforall.cn

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022年5月1,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
文件存储
文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档