您的位置:首页 > 编程语言 > ASP

Asp.net MVC 3 防止 Cross-Site Request Forgery (CSRF)原理及扩展

2011-12-09 20:23 666 查看
Cross-SiteRequestForgery(CSRF)是我们Web站点中常见的安全隐患。下面我们在Asp.netMVC3来演示一下。例如我们有一个HomeContoller中一个SubmitAction,我们标记了HttpPost

[code][HttpPost]


publicActionResultSubmit(FormCollectionfc)


{


if(!string.IsNullOrEmpty(fc["Title"]))


{


ViewBag.Message="Submitsuccess!";


returnView("Index");


}


returnView("Error");


}

[/code]

在View使用Razor简单提交是这样:

[code]@using(Html.BeginForm("Submit","Home"))


{


@Html.TextBox("Title","text");


<inputtype="submit"value="Submit"id="sb1"/>


}

[/code]

点击这个Button我们就提交表单了,接下来我们轻易使用Fiddler来伪造这个HttpPost请求:





然后提交,成功了,返回OK.

POSThttp://localhost:55181/Home/SubmitHTTP/1.1
User-Agent:Fiddler
Host:localhost:55181
Content-Length:10

Title=text

那在Asp.netMVC3WebApplication中如何防止呢?在View中使用


@Html.AntiForgeryToken()


这时当Web应用程序运行时,查看生成HTML,你会看到form标签后有一个hiddeninput标签

[code]<formaction="/Home/Submit2"method="post">


<inputname="__RequestVerificationToken"type="hidden"


value="WiB+H5TNp6V27ALYB3z/1nkD9BLaZIBbWQOBEllj2R/+MkGZqOjLbIof2MJeEoyUJV2ljujNR4etYV6idzji


G4+JL77P9qmeewc4Erh8LnMBHX6zLas2L67GDhvCom0dpiDZl0cH+PykIC/R+HYzEIUTK/thXuF8OUtLwIfKdly0650U


3I7MD6/cIc5aersJBMZ/p6gv76gc6nvKJDt2w0eMy3tkEfAcnNPTdeWr59Ns+48gsGpZ2GSh6G+Uh7rb"/>


<inputid="Title"name="Title"type="text"value="text"/>


<br/><inputtype="submit"value="Submit"id="sb1"/>

[/code]

看源代码是GetHtml方法序列化相应值生成的,

[code]publicHtmlStringGetHtml(HttpContextBasehttpContext,stringsalt,stringdomain,stringpath)


{


Debug.Assert(httpContext!=null);




stringformValue=GetAntiForgeryTokenAndSetCookie(httpContext,salt,domain,path);


stringfieldName=AntiForgeryData.GetAntiForgeryTokenName(null);




TagBuilderbuilder=newTagBuilder("input");


builder.Attributes["type"]="hidden";


builder.Attributes["name"]=fieldName;


builder.Attributes["value"]=formValue;


returnnewHtmlString(builder.ToString(TagRenderMode.SelfClosing));


}

[/code]

同时还写Cookies

__RequestVerificationToken_Lw__=T37bfAdCkz0o1iXbAvH4v0bdpGQxfZP2PI5aTJgLL
/Yhr3128FUY+fvUPApBqz7CGd2uxPiW+lsZ5tvRbeLSetARbHGxPRqiw4LZiPpWrpU9XY8NO4aZzNAdMe+l3q5EMw2iIFB/6UfriWxD7X7n
/8P43LJ4tkGgv6BbrGWmKFo=

更多细节,请查询源代码。然后在Action上增加[ValidateAntiForgeryToken]就可以了,它是这样工作的:





[code]publicvoidValidate(HttpContextBasecontext,stringsalt){


Debug.Assert(context!=null);




stringfieldName=AntiForgeryData.GetAntiForgeryTokenName(null);


stringcookieName=AntiForgeryData.GetAntiForgeryTokenName(context.Request.ApplicationPath);




HttpCookiecookie=context.Request.Cookies[cookieName];


if(cookie==null||String.IsNullOrEmpty(cookie.Value)){


//error:cookietokenismissing


throwCreateValidationException();


}


AntiForgeryDatacookieToken=Serializer.Deserialize(cookie.Value);




stringformValue=context.Request.Form[fieldName];


if(String.IsNullOrEmpty(formValue)){


//error:formtokenismissing


throwCreateValidationException();


}


AntiForgeryDataformToken=Serializer.Deserialize(formValue);




if(!String.Equals(cookieToken.Value,formToken.Value,StringComparison.Ordinal)){


//error:formtokendoesnotmatchcookietoken


throwCreateValidationException();


}




stringcurrentUsername=AntiForgeryData.GetUsername(context.User);


if(!String.Equals(formToken.Username,currentUsername,StringComparison.OrdinalIgnoreCase)){


//error:formtokenisnotvalidforthisuser


//(don'tcareaboutcookietoken)


throwCreateValidationException();


}




if(!String.Equals(salt??String.Empty,formToken.Salt,StringComparison.Ordinal)){


//error:customvalidationfailed


throwCreateValidationException();


}


}

[/code]

从Cookie中获得之前序列化存入的Token,然后反序列化与表单提交的Token进行对比。接着,又对当前请求的用户认证进行确认。最后看有没有设置Salt,有的话再进行比较。其中有一步验证没有通过,则throw异常。
有时的需求是这样的,我们需要使用Session验证用户,那么我们可在上面方法修改增加下面的代码块,意图是对比之前Session值是否与当前认证后Session值相等:



[code]//verifysession


if(!String.Equals(formToken.SessionId,AntiForgeryData.GetGUIDString(),StringComparison.Ordinal))


{


throwCreateValidationException();


}

[/code]

在修改AntiForgeryDataSerializer类,它负责序列化,这里我们增加了SessionId属性:



[code]internalclassAntiForgeryDataSerializer


{


[SuppressMessage("Microsoft.Usage","CA2202:Donotdisposeobjectsmultipletimes",Justification="MemoryStreamisresilienttodouble-Dispose")]


publicvirtualAntiForgeryDataDeserialize(stringserializedToken)


{


if(String.IsNullOrEmpty(serializedToken))


{


thrownewArgumentException("Argument_Cannot_Be_Null_Or_Empty","serializedToken");


}




try


{


using(MemoryStreamstream=newMemoryStream(Decoder(serializedToken)))


using(BinaryReaderreader=newBinaryReader(stream))


{


returnnewAntiForgeryData


{


Salt=reader.ReadString(),


Value=reader.ReadString(),


CreationDate=newDateTime(reader.ReadInt64()),


Username=reader.ReadString(),


SessionId=reader.ReadString()


};


}


}


catch(Exceptionex)


{


thrownewSystem.Web.Mvc.HttpAntiForgeryException("AntiForgeryToken_ValidationFailed",ex);


}


}




[SuppressMessage("Microsoft.Usage","CA2202:Donotdisposeobjectsmultipletimes",Justification="MemoryStreamisresilienttodouble-Dispose")]


publicvirtualstringSerialize(AntiForgeryDatatoken)


{


if(token==null)


{


thrownewArgumentNullException("token");


}




using(MemoryStreamstream=newMemoryStream())


using(BinaryWriterwriter=newBinaryWriter(stream))


{


writer.Write(token.Salt);


writer.Write(token.Value);


writer.Write(token.CreationDate.Ticks);


writer.Write(token.Username);


writer.Write(token.SessionId);




returnEncoder(stream.ToArray());


}


}


}

[/code]

在View这样使用,并引入Salt,这使得我们安全机制又提升了一点儿。



[code]@using(Html.BeginForm("Submit2","Home"))


{


@Html.AntiForgeryToken(DebugMvc.Controllers.Config.SALT);


@Html.TextBox("Title","text");


<br/>


<inputtype="submit"value="Submit"id="sb1"/>




}

[/code]

Action的特性上,我们也配置对应的Salt字符串:

[code][HttpPost]


[ValidateAntiForgeryToken(Salt=Config.SALT)]


publicActionResultSubmit2(FormCollectionfc)


{


if(!string.IsNullOrEmpty(fc["Title"]))


{


ViewBag.Message="Submitsuccess!";


returnView("Index");


}


returnView("Error");


}

[/code]



[code]配置类:
publicclassConfig


{


publicconststringSALT="Whyyouarehere";


}

[/code]

这个实现一个简单的Session在HttpModule中,



[code]publicclassMySessionModule:IHttpModule


{


#regionIHttpModuleMembers




publicvoidDispose(){}




publicvoidInit(HttpApplicationcontext)


{


context.AcquireRequestState+=newEventHandler(this.AcquireRequestState);


}




#endregion




protectedvoidAcquireRequestState(objectsender,EventArgse)


{


HttpApplicationhttpApp=(HttpApplication)sender;


if(httpApp.Context.CurrentHandlerisIRequiresSessionState)


{


if(httpApp.Session.IsNewSession)


{


httpApp.Session["GUID"]=Guid.NewGuid();


}




}


}


}

[/code]

这时我们再使用Fiddler模拟请求POST到这个Action,后得到下面的结果,这个异常信息也是可以修改的:

AntiForgeryToken_ValidationFailed

Description:Anunhandledexceptionoccurredduringtheexecutionofthecurrentwebrequest.Pleasereviewthestacktraceformoreinformationabouttheerrorandwhereitoriginatedinthecode.
ExceptionDetails:System.Web.Mvc.HttpAntiForgeryException:AntiForgeryToken_ValidationFailed

最后让我们来看单元测试的代码:



[code]namespaceDebugMvc.Ut


{


usingSystem;


usingSystem.Collections.Generic;


usingSystem.Linq;


usingSystem.Web;


usingMicrosoft.VisualStudio.TestTools.UnitTesting;


usingDebugMvc.Controllers;


usingSystem.Web.Mvc;


usingMoq;


usingSystem.Collections.Specialized;


usingMatch=System.Text.RegularExpressions.Match;


usingSystem.Text.RegularExpressions;


usingSystem.Globalization;




[TestClass]


publicclassUnitTestForAll


{


privatestaticstring_antiForgeryTokenCookieName=AntiForgeryData.GetAntiForgeryTokenName("/SomeAppPath");


privateconststring_serializedValuePrefix=@"<inputname=""__RequestVerificationToken""type=""hidden""value=""Creation:";


privateconststring_someValueSuffix=@",Value:somevalue,Salt:someothersalt,Username:username""/>";


privatereadonlyRegex_randomFormValueSuffixRegex=newRegex(@",Value:(?<value>[A-Za-z0-9/\+=]{24}),Salt:someothersalt,Username:username""/>$");


privatereadonlyRegex_randomCookieValueSuffixRegex=newRegex(@",Value:(?<value>[A-Za-z0-9/\+=]{24}),Salt:");




[TestMethod]


publicvoidTestValidateAntiForgeryToken2Attribute()


{


//arrange


varmockHttpContext=newMock<HttpContextBase>();




varcontext=mockHttpContext.Object;


varauthorizationContextMock=newMock<AuthorizationContext>();


authorizationContextMock.SetupGet(ac=>ac.HttpContext).Returns(context);




boolvalidateCalled=false;


Action<HttpContextBase,string>validateMethod=(c,s)=>


{


Assert.AreSame(context,c);


Assert.AreEqual("somesalt",s);


validateCalled=true;


};


varattribute=newValidateAntiForgeryToken2Attribute(validateMethod)


{


Salt="somesalt"


};




//Act


attribute.OnAuthorization(authorizationContextMock.Object);




//Assert


Assert.IsTrue(validateCalled);


}




[TestMethod]


publicvoidGetHtml_ReturnsFormFieldAndSetsCookieValueIfDoesNotExist()


{


//Arrange


AntiForgeryWorkerworker=newAntiForgeryWorker()


{


Serializer=newDummyAntiForgeryTokenSerializer()


};


varcontext=CreateContext();




//Act


stringformValue=worker.GetHtml(context,"someothersalt",null,null).ToHtmlString();




//Assert


Assert.IsTrue(formValue.StartsWith(_serializedValuePrefix),"Formvalueprefixdidnotmatch.");




MatchformMatch=_randomFormValueSuffixRegex.Match(formValue);


stringformTokenValue=formMatch.Groups["value"].Value;




HttpCookiecookie=context.Response.Cookies[_antiForgeryTokenCookieName];


Assert.IsNotNull(cookie,"Cookiewasnotsetcorrectly.");


Assert.IsTrue(cookie.HttpOnly,"CookieshouldhaveHTTP-onlyflagset.");


Assert.IsTrue(String.IsNullOrEmpty(cookie.Domain),"Domainshouldnothavebeenset.");


Assert.AreEqual("/",cookie.Path,"Pathshouldhaveremainedat'/'bydefault.");




MatchcookieMatch=_randomCookieValueSuffixRegex.Match(cookie.Value);


stringcookieTokenValue=cookieMatch.Groups["value"].Value;




Assert.AreEqual(formTokenValue,cookieTokenValue,"Formandcookietokenvaluesdidnotmatch.");


}




privatestaticHttpContextBaseCreateContext(stringcookieValue=null,stringformValue=null,stringusername="username")


{


HttpCookieCollectionrequestCookies=newHttpCookieCollection();


if(!String.IsNullOrEmpty(cookieValue))


{


requestCookies.Set(newHttpCookie(_antiForgeryTokenCookieName,cookieValue));


}


NameValueCollectionformCollection=newNameValueCollection();


if(!String.IsNullOrEmpty(formValue))


{


formCollection.Set(AntiForgeryData.GetAntiForgeryTokenName(null),formValue);


}




Mock<HttpContextBase>mockContext=newMock<HttpContextBase>();


mockContext.Setup(c=>c.Request.ApplicationPath).Returns("/SomeAppPath");


mockContext.Setup(c=>c.Request.Cookies).Returns(requestCookies);


mockContext.Setup(c=>c.Request.Form).Returns(formCollection);


mockContext.Setup(c=>c.Response.Cookies).Returns(newHttpCookieCollection());


mockContext.Setup(c=>c.User.Identity.IsAuthenticated).Returns(true);


mockContext.Setup(c=>c.User.Identity.Name).Returns(username);




varsessionmock=newMock<HttpSessionStateBase>();


sessionmock.Setup(s=>s["GUID"]).Returns(Guid.NewGuid().ToString());




mockContext.Setup(c=>c.Session).Returns(sessionmock.Object);




returnmockContext.Object;


}


}




internalclassDummyAntiForgeryTokenSerializer:AntiForgeryDataSerializer


{


publicoverridestringSerialize(AntiForgeryDatatoken)


{


returnString.Format(CultureInfo.InvariantCulture,"Creation:{0},Value:{1},Salt:{2},Username:{3}",


token.CreationDate,token.Value,token.Salt,token.Username);


}


publicoverrideAntiForgeryDataDeserialize(stringserializedToken)


{


if(serializedToken=="invalid")


{


thrownewHttpAntiForgeryException();


}


string[]parts=serializedToken.Split(':');


returnnewAntiForgeryData()


{


CreationDate=DateTime.Parse(parts[0],CultureInfo.InvariantCulture),


Value=parts[1],


Salt=parts[2],


Username=parts[3]


};


}


}


}

[/code]

这里只是UnitTest的一部分,使用Moq来实现MockHttpContext,从而实现对HttpContext的单元测试。

小结:Web站点的安全问题,不可轻视。特别现在Ajax大量应用,做好安全检测很重要。

希望对您Web开发有帮助。

作者:PetterLiu
出处:http://www.cnblogs.com/wintersun/
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
该文章也同时发布在我的独立博客中-PetterLiuBlog。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
章节导航