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]
在View使用Razor简单提交是这样:
[/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()
[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.
POST
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开发有帮助。
作者:
出处:
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
该文章也同时发布在我的独立博客中-
相关文章推荐
- Asp.net MVC 3 防止 Cross-Site Request Forgery (CSRF)原理及扩展 安全 注入
- [水煮 ASP.NET Web API2 方法论](1-7)CSRF-Cross-Site Request Forgery
- [水煮 ASP.NET Web API2 方法论](1-7)CSRF-Cross-Site Request Forgery
- Cross-Site Request Forgery (CSRF)
- WebGoat学习——跨站请求伪造(Cross Site Request Forgery (CSRF))
- 转: CSRF(Cross Site Request Forgery 跨站域请求伪造) 背景与介绍
- Anti-Forgery Request Recipes For ASP.NET MVC And AJAX
- 我要学ASP.NET MVC 3.0(十三): MVC 3.0 防止跨站点请求伪造 (CSRF) 攻击
- Cross Site Request Forgery (CSRF)--spring security -转
- WebGoat学习——跨站请求伪造(Cross Site Request Forgery (CSRF))
- CSRF (Cross Site Request Forgery)
- 【MVC整理】4.Asp.net MVC 如何防止CSRF攻击
- ASP.NET MVC 防止 CSRF 的方法
- ASP.NET MVC 防止 CSRF 的方法
- CSRF(Cross Site Request Forgery, 跨站域请求伪造)
- ASP.NET MVC 防止 CSRF 的方法
- ASP.NET MVC中防止跨站请求攻击(CSRF)
- 我要学ASP.NET MVC 3.0(十三): MVC 3.0 防止跨站点请求伪造 (CSRF) 攻击
- 每日一记======>(转)CSRF介绍(Cross-site request forgery)以及django中的处理方法
- 浅谈ASP.NET MVC 防止跨站请求伪造(CSRF)攻击的实现方法