在網站開發中,須要注意的一個問題就是防範XSS攻擊,Asp.net mvc中已經自動爲咱們提供了這個功能。用戶提交數據時時,在生成Action參數的過程當中asp.net會對用戶提交的數據進行驗證,一旦發現提交的數據中包含了XSS攻擊的代碼,就會拋出異常,用戶在這時候就會看到一個出錯頁面。這種默認的行爲保證了網站的安全性,可是對於用戶體驗來講卻不夠友好,因此大多數人都但願對用戶進行提示,或者對提交的數據進行過濾,移除掉XSS攻擊的代碼。html
對於此類問題,網上有不少人問過,經過百度搜索出來的解決方法好多都只提到了「關閉頁面數據驗證」。確實,關閉了頁面數據驗證後,用戶提交的任何數據都會到達服務器端的處理程序,在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攻擊代碼過濾。mvc
ValidationAttribute是全部驗證屬性的基類,RangeAttribute, RequiredAttribute, StringLengthAttribute都是它的子類,這個類的中包有一個名爲IsValid的方法,來對數據進行驗證,方法聲明以下:框架
protected virtual ValidationResult IsValid(Object value, ValidationContext validationContext)
參數value即爲要驗證的對象,參數ValidationContext爲驗證上下文,此類包含了較多的信息,比較重要的有屬性ObjectInstance和MemberName。asp.net
其中ValidationContext的ObjectInstance屬性可獲取要驗證的對象,而MemberName可獲取或設置要驗證的成員名稱。這裏要進行一下解釋,按照我上面的說法,value是要驗證的對象,ValidationContext.ObjectInstance也是要驗證的對象,難道它們兩者是同一個對象麼,答案是No,(不是我故意要把他們表達成一個意思,而是MSDN太坑,本段開頭摘自MSDN),對於咱們示例中的PersonModel類型來講,因爲其是一個複雜類型,因此最終的驗證會落到它的各個屬性上,假如要驗證屬性Name,參數value即爲屬性Name的值,而ValidationContext.ObjectInstance則爲一個PersonModel的實例,ValidationContext.MemberName的值按照MSDN的解釋,應該是一個字符串「Name」;這下你們清楚兩者的區別了吧。我之因此說假如要驗證屬性Name,是由於屬性Name上如今尚未任何的驗證特性(AllowHtmlAttribute不是一個驗證特性)。ide
到這裏我想可能有的人已經想到我要怎麼作了,在這裏我得到了屬性值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 、
最終我實現了本身的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; }}