在網站開發中,需要注意的一個問題就是防范XSS攻擊,Asp.net mvc中已經自動為我們提供了這個功能。用戶提交數據時時,在生成Action參數的過程中asp.net會對用戶提交的數據進行驗證,一旦發現提交的數據中包含了XSS攻擊的代碼,就會拋出異常,用戶在這時候就會看到一個出錯頁面。這種默認的行為保證了網站的安全性,但是對於用戶體驗來說卻不夠友好,所以大多數人都希望對用戶進行提示,或者對提交的數據進行過濾,移除掉XSS攻擊的代碼。
對於此類問題,網上有很多人問過,通過百度搜索出來的解決方法好多都只提到了“關閉頁面數據驗證”。確實,關閉了頁面數據驗證后,用戶提交的任何數據都會到達服務器端的處理程序,在asp.net mvc中這一點可以通過在model的相應屬性上附加AllowHtmlAttribute或者在Action上附加ValidateInputAttribute(false)來實現。但是比關閉頁面數據驗證更重要的一點是,關閉之后,這個數據驗證和處理的重擔就要由程序員來承擔了。
解決這個問題最直接的方法就是在每一個要處理提交數據的Action的開始,對相應的參數進行過濾,對於XSS攻擊代碼的過濾,可以使用微軟發布的名為AntiXss的類庫,通過Nuget可以獲取該類庫,在我的解決方法中也是使用此類庫進行過濾的。
我新建了一個Asp.net mvc項目進行演示,只有一個Controller名字為PersonController,一個Model,名字為PersonModel,PersonController中只有兩個Action,全部代碼如下。
public class PersonModel
{ [AllowHtml] //別忘了AllowHtmlAttribute,要不然提交數據就報錯了 public string Name { get; set; } public int Age { get; set; } } public class PersonController : Controller { public ActionResult Index() { return View(); } public ActionResult Save(PersonModel model) { //Sanitizer為AntiXss類庫提供的靜態類,用於過濾XSS代碼 model.Name = Sanitizer.GetSafeHtmlFragment(model.Name); //保存到數據庫中 return Content("Success"); } }
視圖文件Index.cshtml內容如下
@model AntiXss.Models.PersonModel @{ ViewBag.Title = "Index"; } <h2>Index</h2> @using (Html.BeginForm("Save","Person",FormMethod.Post)) { @Html.LabelFor(model=>model.Name); @Html.EditorFor(model=>model.Name); <br/> @Html.LabelFor(model=>model.Age) @Html.EditorFor(model=>model.Age) <input type="submit" value="submit"/> }
這樣的代碼無疑是可以達到我們過來XSS攻擊的目的的,但是在實際項目中,Controller往往有數十個,Action的數目更是成百上千,而且ViewModel的屬性又往往很多,如果我們按照上面的方式逐個Action的逐個Model的屬性進行處理,代碼會變得又臭又長,而且還容易遺漏。使用這種方式來進行過濾實在是一種自虐行為呀。
優秀的程序員都是懶漢,對於這種繁瑣的體力勞動,一定要想方設法地避免。在asp.net mvc中,給我們提供了很多工具以實現aop編程,最常用的就是各種Filter了,所以在解決此問題時,我就想是否可以利用asp.net mvc提供的aop編程來實現XSS過濾,經過思考和翻閱蔣金楠的《ASP.NET MVC4框架揭秘》,最終找到了一種較好的解決方式,就是通過ValidationAttribute來實現XSS攻擊代碼過濾。
ValidationAttribute是所有驗證屬性的基類,RangeAttribute, RequiredAttribute, StringLengthAttribute都是它的子類,這個類的中包有一個名為IsValid的方法,來對數據進行驗證,方法聲明如下:
protected virtual ValidationResult IsValid(Object value, ValidationContext validationContext)
參數value即為要驗證的對象,參數ValidationContext為驗證上下文,此類包含了較多的信息,比較重要的有屬性ObjectInstance和MemberName。
其中ValidationContext的ObjectInstance屬性可獲取要驗證的對象,而MemberName可獲取或設置要驗證的成員名稱。這里要進行一下解釋,按照我上面的說法,value是要驗證的對象,ValidationContext.ObjectInstance也是要驗證的對象,難道它們二者是同一個對象么,答案是No,(不是我故意要把他們表達成一個意思,而是MSDN太坑,本段開頭摘自MSDN),對於我們示例中的PersonModel類型來說,由於其是一個復雜類型,所以最終的驗證會落到它的各個屬性上,假如要驗證屬性Name,參數value即為屬性Name的值,而ValidationContext.ObjectInstance則為一個PersonModel的實例,ValidationContext.MemberName的值按照MSDN的解釋,應該是一個字符串“Name”;這下大家清楚二者的區別了吧。我之所以說假如要驗證屬性Name,是因為屬性Name上現在還沒有任何的驗證特性(AllowHtmlAttribute不是一個驗證特性)。
到這里我想可能有的人已經想到我要怎么做了,在這里我獲得了屬性值value,也獲得了包含該屬性的實例ValidationContext.ObjectInstance,接下來我要做的就是將該屬性值進行修改就可以了,修改屬性值可以通過反射輕松實現,所以我的用於過濾XSS攻擊代碼的自定義驗證屬性就寫出來了,如下
public class AntiXssAttribute :ValidationAttribute { protected override ValidationResult IsValid(object value, ValidationContext validationContext) { //對於XSS攻擊,只需要對string類型進行驗證就可以了 var str = value as string; if (!string.IsNullOrWhiteSpace(str) && validationContext.ObjectInstance != null && ! string.IsNullOrWhiteSpace(validationContext.MemberName)) { str = Sanitizer.GetSafeHtmlFragment(str); PropertyInfo pi = validationContext.ObjectType.GetProperty(validationContext.MemberName, BindingFlags.Public | BindingFlags.Instance); pi.SetValue(validationContext.ObjectInstance,str); } //由於這個類的目的並不是為了驗證,所以返回驗證成功 return ValidationResult.Success; } }
然后我們將這個自定義的驗證特性附加到PersonModel的Name屬性上(一定不要刪除AllowHtmlAttribute,要不然提交包含html標簽或者js代碼的數據時會出錯的),當用戶提交數據時,asp.net在進行model驗證時就會自動為我們過濾XSS攻擊代碼了,一切看起來都是那么的美好,可是事實並非如此!!
當程序運行時,用戶提交的XSS代碼並沒有被過濾,原因是ValidationContext.MemberName屬性根本不存在,這實在是微軟的一個坑,MSDN告訴我們通過這個屬性可以獲取或設置要驗證的成員名稱,可是其實自始至終根本沒有代碼來設置這個屬性值,這個屬性值一直都是null,所以要想讓我們的代碼順利進行,我們要想辦法給ValidationContext.MemberName賦值才可以,要給ValidationContext的這個屬性賦值,自然要在實例化它的地方。對於ValidationContext對象的實例化,我在這里不贅述,因為這涉及到Asp.net mvc的模型驗證機制,這一點蔣金楠的博文早就講清楚了,而我也自認為不會講的比他更清楚,想了解的人請閱讀蔣金楠的博客ASP.NET MVC以ModelValidator為核心的Model驗證體系: ModelValidator 、
ASP.NET MVC基於標注特性的Model驗證:DataAnnotationsModelValidator 和
ASP.NET MVC基於標注特性的Model驗證:DataAnnotationsModelValidatorProvider。
最終我實現了自己的AntiXssDataAnnotationsModelValidator和AntiXssDataAnnotationsModelValidatorProvider,在AntiXssDataAnnotationsModelValidator中實例化了ValidationContext對象,並且為該對象的MemberName屬性賦值。
public class AntiXssDataAnnotationsModelValidator:DataAnnotationsModelValidator { public AntiXssDataAnnotationsModelValidator(ModelMetadata metadata,ControllerContext context,AntiXssAttribute attribute) :base(metadata,context,attribute) { } public override IEnumerable<ModelValidationResult> Validate(object container) { var validationContext = new ValidationContext(container ?? base.Metadata.Model, null, null); validationContext.DisplayName = base.Metadata.GetDisplayName(); validationContext.MemberName = base.Metadata.PropertyName; ValidationResult validationResult = this.Attribute.GetValidationResult(base.Metadata.Model, validationContext); yield break; } } public class AntiXssDataAnnotationsModelValidatorProvider : DataAnnotationsModelValidatorProvider { protected override IEnumerable<ModelValidator> GetValidators(ModelMetadata metadata, ControllerContext context, IEnumerable<Attribute> attributes) { foreach (var attribute in attributes.OfType<AntiXssAttribute>()) { yield return new AntiXssDataAnnotationsModelValidator(metadata,context,attribute); } } }
然后記得在Global.asax中對這個AntiXssDataAnnotationsModelValidatorProvider 進行注冊。
最后我又對AntiXssAttribute類進行了一點修改,為了在標記了該特性時不需要再額外地標記AllowHtmlAttribute:
public class AntiXssAttribute :ValidationAttribute, IMetadataAware{ protected override ValidationResult IsValid(object value, ValidationContext validationContext) { //對於XSS攻擊,只需要對string類型進行驗證就可以了 var str = value as string; if (!string.IsNullOrWhiteSpace(str) && validationContext.ObjectInstance != null && ! string.IsNullOrWhiteSpace(validationContext.MemberName)) { str = Sanitizer.GetSafeHtmlFragment(str); PropertyInfo pi = validationContext.ObjectType.GetProperty(validationContext.MemberName, BindingFlags.Public | BindingFlags.Instance); pi.SetValue(validationContext.ObjectInstance,str); } //由於這個類的目的並不是為了驗證,所以返回驗證成功 return ValidationResult.Success; } public void OnMetadataCreated(ModelMetadata metadata) { //實際上AllowHtmlAttribute也是實現了接口IMetadataAware,在OnMetadataCreated //中使用了如下的代碼 metadata.RequestValidationEnabled = false; }}