https://newera17031.activehosted.com/index.php?action=social&chash=ba1b3eba322eab5d895aa3023fe78b9c.2767&nosocial=1
https://newera17031.activehosted.com/index.php?action=social&chash=ba1b3eba322eab5d895aa3023fe78b9c.2767&nosocial=1

觀點

鞏固系統防禦首重資料驗證

2009 / 07 / 16
李泳泉
鞏固系統防禦首重資料驗證

系統也必須要能鞏固自己的防禦,才能在惡意使用者的圍剿下生存;而資料驗證正是鞏固系統防禦最重要的方法。

  孫子兵法軍形篇說道:「昔之善戰者,先為不可勝,以待敵之可勝」;在戰場上,要先使自己不可被戰勝,才有機會戰勝別人。同樣的,系統也必須要鞏固自己的防禦,才能在惡意使用者的圍剿下生存;而資料驗證正是鞏固系統防禦最重要的方法。

  「所有的輸入都是惡意的」是微軟的安全專家在Writing Secure Code這本書所提出的至理名言,也一語道出資料驗證的必要性;然而由近幾年來Cross-Site Scripting(XSS)及Injection Flaw等注入式弱點排名在OWASP Top 10中不降反升的趨勢可看出,資料驗證在許多系統中沒有被落實。因此本文嘗試將資料驗證的議題作整理、劃分種類、並提出可行的實作建議。

資料驗證的種類

  資料驗證的目的是檢驗資料的合法性;對系統而言,資料合法性不外乎以下3個:(1)資料一致性;(2)是否包含惡意的字元;(3)是否合乎特定特徵或商業邏輯。

(1) 資料一致性

  驗證資料在傳輸過程是否遭到竄改?對於某些使用Cookie或HTTP Header作為通過認證依據的單一簽入機制,若Cookie或Header內容遭到竄改可能造成惡意使用者偽裝其使用者或不當的提升權限。

(2) 資料中是否包含惡意的字元

  包含HTML或Script的資料內容(如…<aonClick=”…”>…)可能造成XSS攻擊;而包含單引號('')或兩個連字符號()的輸入可能造成SQL Injection效果;除非系統有特殊需求,一般資料內容不應包含這些字元。

(3) 資料是否符合特定特徵或格式

  大多數的系統資料都可以被規範出其應具有的資料特徵(例如型別、長度、範圍等)或是應符合的格式(例如Email格式、中華民國身份證字號格式等);系統應該要可以識別資料是否符合該特徵,以決定是否使用該資料。

  不同的合法性有不同的驗證方式;在實作資料驗證時我們必須明確的知道該資料所應俱備的合法性,才能夠選擇適當的驗證邏輯及實作方法;例如身份證字號的輸入資料應該被驗證是否符合身份證字號格式。實作方法稍後會有詳細描述。

資料驗證的範圍及界限

  為增加可維護性及修改的彈性,現在的應用系統大多採取多層式(Multi-tier)設計;但是要在何處實作資料驗證卻也成為許多系統設計師難解的問題。最理想的方法是每層都要實作資料驗證,但是耗費的工時、以及對功能和效能可能造成的影響讓理想的方法永遠只是理想。

  Writing Secure Code建議我們應該定義信任邊界(Trust Boundary),在邊界內的資料都是可以信任的。邊界的範圍依情況有所不同,但我們建議邊界的最大界限不應跨越系統。

  應用系統內的各層可以信任彼此提供的資料;但是一旦跨越邊界的檢核點(Checkpoint),資料就應該被驗證。

資料驗證的方法

  資料驗證常用的方法如下3種;並與前述3個資料的合法性連結,說明其適用性。

(1) 使用雜湊演算法驗證資料完整性

  驗證資料一致性的目的在於確認資料在傳輸過程中完整性未遭非人為(accidental)破壞,和原始的值相同。使用雜湊(Hash)演算法是確保資料完整性的常用作法。

  舉例來說,兩系統之間使用Cookie傳送認證資料(例如登入者的ID),Cookie1以明文方式存放登入者ID,另一個Cookie2則存放使用雜湊演算法運算過的登入者ID;另一個系統接收到兩組Cookie後,先將Cookie1的內容用同樣的雜湊演算法運算後,再與Cookie2的內容比較,若兩者一致則代表傳輸過程中未遭非人為破壞。

  目前軟體發佈時常用MD5作為雜湊演算法;但MD5已被證明可找到雜湊碰撞(Hash Collision)(註1),亦即已被破解,因此在演算法選擇上建議使用強度較高的雜湊演算法(如SHA256),以增加破解難度。如果要偵測資料在傳輸過程中,遭到人為蓄意(intentional)的篡改,則必須使用包含金鑰運算的雜湊演算法(Keyed Hash),如HMACSHA1。若使用Keyed Hash演算法,在運算之前雙方系統要先交換金鑰,或據以產生金鑰的參數,如圖2中的Salt(亂數值)及Password。

(2)使用黑名單阻擋不合法的資料輸入

  所謂黑名單的作法是以思考「我的系統不能接受那些字元或字元組合的輸入」為出發點來進行資料驗證。黑名單的實作可以分為兩大類:(1)檢查並封鎖;(2)消毒(Sanitize)。

?檢查及封鎖

  檢查資料內容是否包含不合法的字元或字串,若發現即拒絕該資料進入系統;最簡單的方式是使用String類別內建的方法(如Contains)來檢查資料中是否包含不合法字串,若要檢查的字串樣式(Pattern)較複雜;例如要檢查是否包含以<開頭,以>結尾的字串;則需要使用正規表示式(RegularExpression)來進行檢查。

  ASP.NET本身所具備的驗證Request內容的功能就是屬於檢查及封鎖的類型。

?消毒(Sanitize)

  不對資料進行檢查,而是直接對資料進行消毒;也就是使用取代或編碼的方式將有害的字元過濾掉;例如將上引號('')取代為兩個上引號(”)來阻止SQL njection,或是使用Server.HtmlEncode將小於符號(<)取代成&lt;來防止XSS攻擊的發生。

(3)使用白名單列舉系統可接受的資料特徵

  相對於黑名單,白名單的作法是以思考「我的系統只能接受符合特定特徵的輸入」為出發點來進行資料驗證。

  資料特徵包含型別(文字或數字)、最大或最小長度、範圍(若為數字或日期)、以及特定格式(Email格式、身份證字號格式)。驗證資料特徵最簡易的方法是用Framework本身提供的函式來實作型別或長度的檢查;例如.NET的基本資料型別,包括Int16、Single等類別都有提供TryParse方法,Java則可透過基本資料型別的外覆(Wrapper)類別所提供的ParseInt、ParseFloat等方法來實作。

  若要檢查的資料特徵較複雜;例如要檢查是否符合Email格式;則需要使用正規表示式來檢查。

  在驗證合法性的實作上,雜湊演算法適合用來驗證資料的一致性;黑名單的作法較適合驗證資料中是否包含不合法的字元;白名單的作法則適合資料特徵或資料邏輯的驗證。

  在實務上,黑名單的作法比較容易產生漏洞,因為資料的樣式可以有許多變化,例如C14N的議題(註2)、URL Encode;黑名單的檢查清單可能無法包含所有的變化而無法阻擋惡意輸入。因此我們建議系統應搭配黑名單與白名單作法,讓惡意使用者難以趁虛而入。

資料驗證實作建議

  在現在大多採用多層式架構設計的系統中,大致都可分為呈現層、商業邏輯層和資料存取層;. N E T和J 2 E E架構還有支援F i l t e r(. N E T為HttpHandler;J2EE為Filter Servlet)的實作,允許程式在使用者的Request進入系統之前進行某些處理,例如編碼轉換。由於每一層的責任及功能不同、資料特性也異,因此在每一層的資料驗證策略也有所不同。接著我們來檢視各層的責任及資料特性,及建議適合的策略及實作方式。

  在進行檢視之前先說明一個概念;注入式攻擊,如XSS或SQL Injection之所以能成功是由於系統將資料當成指令的一部份來執行;資料驗證是針對資料進行,而非指令。因此在各層中,資料和指令是否能被區分也是資料驗證的重要考量。

?Filter

  Filter會在所有Request進入系統前先對資料進行處理;雖然在Filter中資料和指令的區別明確,但是在Filter一般不會實作商業邏輯,所以Filter較適合實作資料一致性的驗證、以及使用黑名單過濾Request中的不合法字元。若是在Filter中實作資料邏輯驗證,可能讓Filter的程式過於複雜而造成效能問題,若是因驗證規則錯誤也可能影響系統的全部功能。

?呈現層(Presentation Tier)

  呈現層是和使用者互動的第一線;可以清楚的區分資料和指令,同時也已清楚資料特徵及格式,因此適合使用白名單實作資料特徵及格式的驗證。若是架構不支援Filter,使用黑名單方式過濾不合法字元的功能也可能在此實作。在呈現層實作資料驗證的缺點是驗證程式會分散在所有頁面,實作和管理較費時;同時也需要處理錯誤的資訊呈現。因此在實作上建議使用Framework提供的既有元件,例如ASP.NET的Validation Control或Java Struts的Validation模組。

?商業邏輯層(Business Logic Tier)

  商業邏輯層負責處理系統中商業流程的執行,在此亦可清楚區分資料和指令,同時也應該清楚資料特徵及商業邏輯;因此除了呈現層之外,商業邏輯層亦適合實作資料特徵及商業邏輯的驗證。由於不需要處理錯誤的資訊呈現,因此可以使用程式語言提供的方法(如TryParse、ParseInt)或使用正規表示式實作驗證規則。

?資料存取層(Data Access Tier)

  資料存取層負責介接外部儲存媒體,進行資料存取。許多人會認為在此實作資料驗證是最輕鬆的,因為所有資料幾乎都會經過這裡;把守最後的關卡即可。但是從職責來看,資料存取層不應實作過多商業邏輯,若要檢查資料的特性,應該檢查的也只是資料欄位是否可以空白、型態和長度。而且有的實作是在商業邏輯層將SQL字串組好,再交由資料存取層執行;因此有可能在資料存取層已經無法區分資料和指令,所以資料存取層並不適合實作資料驗證。在資料存取層能作的便是使用參數式的資料庫查詢指令(Parameterized SQL Statement)讓資料庫知道那些是資料、那些是指令,以降低SQL Injection發生的機率。

  綜整以上的描述,我們可以得到一張新的架構
圖。

實作案例

  最後以一個實作案例,輔以ASP.NET程式碼範例來實現上面所談的實作策略。我們希望為公司實作一個人力登錄系統,掛在公司入口網站下。求職者可以上系統留下自己的聯絡方式、個人履歷、以及感興趣的職缺等,求職者在登錄資料時不允許使用HTML及JavaScript以避免XSS攻擊;管理者需先由公司入口網站登入,才能管理求職者登錄的資料;人力登錄系統和入口網站是不同的系統,管理者在入口網站登入後,入口網站會以Cookie記錄登入者的帳號,讓人力登錄系統可以識別管理者是否已登入系統。

  由於兩系統間會有Cookie的交換,為避免Cookie在過程中被篡改,因此我們在Filter中驗證Cookie內容的一致性,並且先檢查輸入的參數是否包含不合法的字元。以下程式碼片段以IHttpModule實作Filter功能(如表2)。

  在Filter檢核可以阻擋大部份的不合法字元輸入,但是惡意使用者可能利用URL Encoding的方法來迴避;因此在呈現層我們針對可以明確定義資料特徵的欄位以白名單方式進行檢核。以下程式碼片段顯示在ASPX頁面中使用.NET內建的RegularExpressi onValidator控制項驗證Email欄位的輸入。

<asp:TextBox ID="txtEmail" runat="server" MaxLength="30"></asp:TextBox>
<asp:RegularExpressionValidator ID="valEmail" runat="server"
ControlToValidate="txtEmail"
ErrorMessage="Email 格式不符"
ValidationExpression="\w+([-+.'']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*">
</asp:RegularExpressionValidator>

  履歷欄位的輸入驗證是我們最頭痛的,因為履歷內容可能出現各式各樣的字元;可能是中英文、數字、全型或半型符號,可能無法明確定義出資料特徵,我們可能使用黑名單的方式檢核;但擔心黑名單方式可能無法涵蓋所有的可能性,因此履歷資料在輸出到頁面前應該被編碼;事實上不只履歷資料,所有資料在呈現前都應該被編碼。在此我們使用微軟推出的Anti-Cross Site Scripting Library。

‘以身份證字號取得使用者的履歷資料
Dim resumeStr As String = ResumeManager.GetResumeContent(IdNumber)
''使用Anti-Cross Site Scripting Library在輸出前對資料內容作編碼
ResumeContent.Text = Microsoft.Security.Application.AntiXss.
HtmlEncode(resumeStr)

  商業邏輯層同樣也是一個適合實作資料驗證的選擇,由於不需要處理錯誤的資訊呈現,因此可以使用程式語言提供的函式來實作資料驗證。以下程式碼片段在商業邏輯層中使用Date.TryParse來驗證使用者輸入的生日欄位、以及使用Date.Compare來驗證求職者的年齡是否符合要求。

Dim dateBirthday As Date
If Not Date.TryParse(strBirthday, dateBirthday) Then
Throw New ArgumentException("生日日期格式錯誤")
End If
If Date.Compare(New Date(Date.Now.Year - 30, 1, 1), dateBirthday) < 1 Then
Throw New ArgumentException("此職位求職者年齡應大於30歲")
End If

  資料來到資料存取層時,如前所述,我們不建議再作資料驗證;能作的只有驗證必要欄位是否有值,還有使用參數式的資料庫查詢指令;如以下程
式碼片段所示(如表3)。

結論

  在以往以單機方式執行程式的時代,資料驗證並未受到重視;然而現在大多數的系統都被發佈到網路上,只要有網址就能使用,我們已無法預期使用者的數量,更無法保證使用者是否是善意的;從去年8月的Mass SQL Injection事件中,可發覺許多系統開發過程中仍然忽視資料驗證,導致網站出現漏洞,讓惡意使用者得逞。資料驗證的動作雖然不易實作,卻是保護自己最有效的方法。「所有輸入都是惡意的」,當系統從界限外接收每筆資料時,我們都應謹記這句話。

表1 常見會跨越檢核點的資料
HTTP Request
?Cookie                        ?Header
? G E T D a t a ( Q u e r y String)
? P O S T D a t a ( F o r m Data、Hidden Field、File Upload)
透過Remote技術呼叫或取得的資料
?EJB                                       ?RMI
?NET Remoting                     ?Web Service
?Socket                                  ?RSS
系統之間的資料轉檔
?File ?Database

表2 以IHttpModule實作Filter

Public Class MyHttpModule Implements IHttpModule

Public Sub Init(ByVal context As System.Web.HttpApplication)
Implements System.Web.IHttpModule.Init
‘註冊以DoAcquireRequestState方法處理 Request
AddHandler context.AcquireRequestState, AddressOf
Me.DoAcquireRequestState
End Sub
Private Sub DoAcquireRequestState(ByVal source As Object, ByVal e
As EventArgs)

Dim cookie1 As HttpCookie = request.Cookies("cookie1")
Dim cookie2 As HttpCookie = request.Cookies("cookie2")
''驗證 cookie 的一致性

''比對hash
Dim salt As Byte() = _
Encoding.ASCII.GetBytes(ConfigurationManager.AppSettings("salt"))
''使用預先設定的 salt 及 password 計算出 hash key
Dim key As New
Rfc2898DeriveBytes(ConfigurationManager.AppSettings("password"),
salt)
''將 key 代入 HMACSHA1演算法
Dim myHash As New HMACSHA1(key.GetBytes(16))
myHash.ComputeHash(Encoding.ASCII.GetBytes(cookie1.Value))
If Not
Convert.ToBase64String(myHash.Hash).Equals(cookie2.Value) Then
Throw New ApplicationException("Cookie 一致性驗證失敗")
End If
''使用Regular Expression尋找是否包含不合法字元
Dim pattern As String = "<.+>" ''檢查參數中是否有 <XXX> 的輸入
For Each field As String In request.Params.Keys
For Each value As String In request.Params.GetValues(field)
If Regex.IsMatch(value, pattern, RegexOptions.IgnoreCase)
Then
Throw New ApplicationException("輸入包含非法字元: " + value)
End If
Next
Next
End Sub
End Class

表3 驗證必要欄位是否有值

Public Function SaveResumeData(ByVal id As String, ByVal birthday
As Date, ByVal phone As String, ByVal email As String, ByVal
resumeContent As String) As Integer
‘檢查必填欄位是否為空值
If String.IsNullOrEmpty(id) Then
Throw New ArgumentNullException("ID 不可為空值")
End If
If String.IsNullOrEmpty(resumeContent) Then
Throw New ArgumentNullException("ResumeContent 不可為空值")
End If
Dim connection As New SqlConnection(ConnectionString)
‘使用 parameterized SQL Statement
Dim sql As String = "INSERT INTO RESUMES (USER_ID,
BIRTHDAY, PHONE, EMAIL, RESUMECONTENT) VALUES (@uid, @
birthday, @phone, @email, @resume)"
Dim command As SqlCommand = New SqlCommand(sql,
connection)
command.Parameters("@uid ").Value = id

Return command.ExecuteNonQuery()
End Function


參考資料:
註1 "How to Break MD5 and Other Hash Functions" Xiaoyun Wang;
Hongbo Yu
http://www.infosec.sdu.edu.cn/uploadfile/papers/How%20to%20Break
%20MD5%20and%20Other%20Hash%20Functions.pdf
註2 C14N - Canonicalization
http://en.wikipedia.org/wiki/C14n