2003 年 7 月,計算機應急反應小組協調中心報告了 Microsoft Windows 的 DirectX MI DI 庫中一組危險的漏洞。DirectXMIDI 庫是用於播放 MIDI 格式音樂的底層 Windows 庫。不幸的是,這個庫沒有能力去檢查 MIDI 文件中的所有數據值;text、copyright 或者 MTh
2003 年 7 月,計算機應急反應小組協調中心報告了 Microsoft Windows 的 DirectX
MIDI 庫中一組危險的漏洞。DirectXMIDI 庫是用於播放 MIDI 格式音樂的底層 Windows 庫。不幸的是,這個庫沒有能力去檢查 MIDI 文件中的所有數據值;text、copyright 或者 MThd track 域中錯誤的值可以導致這個庫的失效,而攻擊者就可以利用這一漏洞讓系統去執行他們想要執行的任何代碼。這是特別危險的,因為 Internet Explorer 在察看一個包含 MIDI 文件鏈接的網頁時,會自動加載那個文件並播放它。結果呢?一個攻擊者只需要發布一個網頁,當用戶察看這個網頁時,讓用戶的計算機刪除所有的文件、把所有的機密文件通過電子郵件發送到其他地方、機器崩潰,或者去做任何攻擊者想要做的事情。
檢查輸入 在幾乎所有安全的程序中,您的第一道防線就是檢查您所接收到的每一條數據。如果您能不讓惡意的數據進入您的程序,或者至少不在程序中處理它,您的程序在面對攻擊時將更加健壯。這與防火牆保護計算機的原理很類似;它不能預防所有的攻擊,但它可以讓一個程序更加穩定。這個過程叫做檢查、驗證或者過濾您的輸入。
一個明顯的問題是,在何處執行檢查?是在數據最初進入程序時,或者是在一個低層次的例程在實際使用這些數據時?通常,最好在這兩處都對其進行檢查;這樣,即使一個攻擊者成功地突破了一道防線,他們還會遇到另一條。最重要的規則是所有的數據必須在使用之前被檢查。
誤區:尋找不正確的輸入 安全程序
開發人員一個最大的誤區是嘗試去查找“非法的”數據值。這是不對的,因為攻擊者非常聰明;他們常常會想到出其他的危險數據值。所以應該做的是確定哪些是合法的,檢查數據是否符合定義,拒絕所有不符合定義的數據。為了安全,在開始時應該特別謹慎,只允許您知道合法的數據。畢竟,如果您限制的過於嚴格,用戶很快就會報告說程序不允許合法的數據進入。另一方面,如果你限制的過於寬松,可能得直到程序被破壞您才會發現這一問題。
例如,我們假設您要基於用戶的某個輸入創建文件名。您可能知道不應該允許用戶的輸入中包括“/”,但是僅僅去檢查這一個字符可能是不對的。比如,控制字符呢?空格會不會出問題?如果以破折號開頭呢(在不好的代碼中可能會出問題)?特別的短語會不會出問題?在絕大多數情況下,如果您創建了一個“非法”字符的列表,攻擊者還是可以找到利用您的程序的方法。所以,應該檢查並保證輸入符合你認為是安全的特定模式,而拒絕不符合這個模式的所有輸入。
確定出您所知道的危險值仍不失為一個好主意:您可以用它們(在頭腦中)檢查您的確認例程。這樣,如果您知道使用“/”是危險的,就可以檢查您的模式保證它不會讓這個字符通過。
當然,所有這些都面臨著一個問題:什麼是合法的值?答案部分取決於您所期望的數據類型。所以接下來的幾節我們將討論程序要用到的幾種通用數據類型――以及如何處理它們。
數字 我們從看起來最容易讀的一類信息開始――數字。如果您期望輸入的是一個數字,就確認數據是數字格式――比如,只是針對阿拉伯數字,並且是至少一位阿拉伯數字(您可以使用與正則表達式 ^[0-9]+$ 檢查它)。在大多數情況下會有一個最小值和一個最大值;如果是這樣,要確認數據在合法范圍之內。
不要根據沒有減號這一條件就認為不會有負數。在很多數據讀取例程中,如果讀到一個特別大的數,就會發生"溢出"而變成一個負數。實際上,一個非常聰明的針對 Sendmail 的攻擊正是基於這一原理。Sendmail 會檢查"調試標記"是不是比合法的值大,但是它並沒有去檢查這個值是不是負數。Sendamil 的開發者想當然地認為既然他們不允許使用減號,就不必再去檢查輸入是不是負數了。問題是數據讀取例程會將大於 2^31 的數,比如4,294,967,269 ,轉換成負數。攻擊者可以利用這一點來覆蓋至關重要的數據,並控制 Sendmail。
如果您讀取的浮點數,還有另外需要關注的問題。許多設計用來讀取浮點數的例程可能會允許“NaN”(非數字)這樣的值。這樣實際上會給接下來的處理例程帶來問題,因為任何與這些數據比較的結果都會是假(而且,NaN 與 NaN 也不相等!)。您還需要知道標准 IEEE 浮點數的其他特殊定義,比如正無窮大和負無窮大,負零(還有正零)。所有您的程序沒有考慮到的輸入數據都有可能導致以後被利用。
字符串 同樣,對於字符串您也要確定哪些是合法的,並拒絕所有其他的字符串。通常指定合法字符串最簡單的方法是使用正則表達式:只需正確使用正則表達式編寫描述哪些字符串合法的模式,拋棄那些不符合這個模式的數據。例如,^[A-Za-z0-9]+$ 指定字符串至少為一個字符長,而且只能包括大寫字母、小寫字母和阿拉伯數字0到9(任意的順序)。您可以使用正則表達式來更為詳細地限制所允許的字符串(例如,您可以進一步指定第一個字符可以是哪些字母)。所有的語言都已實現正則表達式的庫;Perl 是基於正則表達式的,對於 C,函數 regcomp(3) 和 regexec(3) 是POSIX.2 標准,並被廣泛應用。
如果您使用正則表達式,一定要明確地指出您要匹配數據的的開始(通常用 ^ 來標識)和結束(通常用 $ 來標識)。如果您忘記了包括 ^ 或者 $,攻擊者就可以在他們的攻擊中嵌入合法的文本通過您的檢查。如果您使用的 Perl,並且使用的它的多行選項(m),要注意:您必須使用 \A 來標識開始,用 \Z 來標識結束,因為多行操作改變了 ^ 和 $ 的含義。
最大的問題是如何明確地指出在字符串中哪些是合法的。通常,您應該盡可能地嚴格。有很多字符都會帶來特定的問題;只要可能,您就不願意允許在程序內部或者最終輸出中有特定含義的那些字符。人們發現這確實很困難,因為在一些情況下有太多的字符可能會帶來問題。
這裡是經常會帶來問題的字符的部分清單:
常規控制字符(字符值小於32): 還特別包括字符0,傳統上稱做 NUL;我把它稱為 NIL 以區別於 C 語言中的 NULL 指針。在 C 語言中 NIL 標記了一個字符串的結束;即便您沒有直接使用 C 語言,許多庫會間接地去調用 C 語言的例程,如果給出了 NIL,就有可能出錯。另一個問題可以被解釋為命令結束的行結束符。不幸的是,有好幾種行結束編碼:基於 UNIX 的系統使用的是換行字符 (0x0a),但是基於 DOS 的系統(包括
windows)使用的是 CP/M 的回車換行 (0x0d 0x0a),Apple MacOS 使用的是回車 (0x0d),許多 IBM 主機(比如 OS/390)使用的是下一行 (0x85),並且有一些程序甚至(錯誤地)使用反 CP/M 標記 (0x0a 0x0d)。
字符值大於127:這些是國際化的字符,但問題是它們可能會有許多可能的含義,所以您需要確保它們被正確地解釋。通常這些都是 UTF-8 編碼的字符,有其自身的復雜性;可以參考本文 後面關於 UTF-8 的討論 。
元字符: 元字符是在您所依賴的程序或庫中——比如命令 shell 或者
SQL——有特定含義的字符。
在您的程序中有特定含義的字符: 例如,用於定界的字符。許多程序將數據存放在文本文件中,使用逗號、制表符或者冒號隔開數據域;您需要拒絕含有這些值的數據或者對其進行編碼。當前,一個常見的問題是小於號 (<),因為 XML 和 HTML 都用到了它。
這不是一個詳盡的清單,並且您經常是需要接受它們中的一部分的。以後的文章將討論當您不得不接收這些字符時如何處理它們。給出這個清單的目的是說服您嘗試去接受盡可能少的數據,並且在接受另一個字符之前要慎重考慮。您接受的字符越少,您給攻擊者制造的難度就越大。
更多的特殊數據類型 當然,還有更多的特殊數據類型。這裡是對其中一部分的一些簡要介紹。
文件名 如果數據是一個文件名(或者用於創建一個文件),應該對其進行嚴格限制。最好不要讓用戶來選擇文件名,如果不得不那樣做,那麼把字符局限於形如 ^[A-Za-z0-9][A-Za-z0-9._\-]*$ 的較小模式。您應該考慮將“/”、控制字符(尤其生成新行的)和前導符“.”(UNIX/Linux 系統中的隱藏文件)等這些字符從合法模式中去掉。以“-”為前導也不好,因為寫得不好的腳本會把它們解釋為選項:如果有一個文件名為“-rf”,那麼在 UNIX/Linux 中執行命令 rm *,將會變成執行 rm -rf *。將“../”從模式中去掉也是一個好主意,使攻擊者無法“跳出”當前目錄。如果可能,不要允許使用通配符(使用字符 *、?、[] 和 {} 來選擇一組文件);攻擊者可以通過創建稀奇古怪的通配模式來讓系統不知如何處理而關閉。
Windows 還有另外一個問題:一些文件名(忽略擴展名和字母的大小寫)總是被認為是物理設備。例如,如果一個程序在任何目錄中試圖去打開“COM1”或者甚至“com1.txt”,將被系統誤解為是嘗試和串口通信。由於我所關心的是類 UNIX 系統,我就不再深入地探討如何解決這個問題了,而且這也沒有什麼意義,因為這只是一個例子,用來說明一種用於檢查的合法字符不足的情況。
本地化 在當今全球經濟的時候,許多程序都允許用戶用於顯示的語言和其他語言相關的特定信息(比如數字格式和字符編碼)。程序通過用戶提供一個“Locale”值來得到這一信息。例如,本地化參數值為“en_US.UTF-8”說明本地化參數使用的語言是 English,使用美國習慣,使用 UTF-8 編碼。本地的類 UNIX 程序從環境變量中(通常是 LC_ALL,但可能更詳細地分為 LC_COLLATE、LC_CTYPE、LC_MONETARY、LC_NUMERIC 和 LC_TIME;其他要檢查的值是 NLSPATH、LANGUAGE、LANG 和 LINGUAS。)得到這一信息。網絡應用可以通過接收語言請求的頭信息或者別的方法來獲得這個信息。
由於用戶可能是一個攻擊者,我們需要對本地化參數值進行驗證。我建議您確保本地化參數匹配以下模式:
^[A-Za-z][A-Za-z0-9_,+@\-\.=]*$
我 如何 來創建這個驗證模式比這個模式本身更有價值。我首先查找了相關的標准和庫文檔來確定一個 正確 的本地化參數應該是什麼樣的。就這一點而言,有很多互相抵觸的標准,所以我必須確保最終的模式可以接受所有這些標准定義的本地化參數。很快我就發現只需要以上列出的字符,限定這個字符集(尤其是第一個字符)可以避免很多問題。然後我考慮了常見的危險字符(比如作為目錄分隔符的“/”,用於“上級目錄”的“..”,用於前導的破折號,或者空的本地化參數),並確認它們被過濾掉。
UTF-8 國際化對程序還有另外一方面的影響:字符編碼。處理文本需要某種約定將字符轉換為計算機實際可以處理的數字;這些約定叫做 字符編碼 。一個特別常見的文本編碼方法是 UTF-8,它是一個優秀的字符編碼方法,本質上可以表示任何語言的任何字符。UTF-8 之所以特別受歡迎是因為它將普通的 ASCII 文本作為它的一個簡單子集。結果是,原來只是設計用於處理 ASCII 的程序可以很簡單地升級到可以處理 UTF-8;在一些情況下這些程序根本不需要修改。
但是,和任何美好的事物一樣,UTF-8 也有其不足。有一些 UTF-8 字符由一個字節來表示,一些用兩個字節來表示,還有一些用三個字節來表示,甚至更多,而程序被假定總是生成最短的可能的表示。可是,許多 UTF-8 讀取器會接收到“過長”的序列;比如,某些三個字節的序列可能被解釋成由一個由兩個字節表示的字符。攻擊者可以利用這一點來“騙過”數據驗證來攻擊程序。您的過濾器可能不允許十六進制的 2F 2E 2E 2F (“/../”),但如果它允許 UTF-8 的十六進制值 2F C0 AE 2E 2F ,程序可能也會把它解釋為“/../”。所以,如果您要接收 UTF-8 文本,您要確認每一個字符都使用最短可能 UTF-8 編碼(拒絕任何不是最短形式的文本)。許多語言有處理這些的工具,您如果自己寫的話也不難。要注意序列"C0 80"是一個可以表示 NIL (字符00)的過長序列;有一些語言(比如
Java)認為這個特定的序列是可以接收的。
電子郵件地址 許多程序必須接收電子郵件地址, 但是處理所有可能的合法電子郵件地址(如 RFC 2882 和 822 所指定的)令人驚訝的困難。Jeffrey Fiedl 的用於檢查電子郵件地址的“短”正則表達式有 4,724 個字符長,即使是這樣還是沒有包括所有的情況。不過,大多數的程序可以是非常嚴格的,只接收一個特別受限子集的電子郵件以正常地工作。在大多數情況下,只要程序可以接收正常的"name@domain"格式的因特網地址(像“
[email protected]”),拒絕像“John Doe <
[email protected]>”這個在技術上合法的地址是沒問題的。Viega 和 Messier 2003 年出版的書中有可以完成這項檢查的子例程。
Cookies 網絡應用程序經常為重要的數據使用 cookie。如我以後將要講到的那樣,不要忘記用戶可以任意地重新設置 cookie 的值和形式,這一點很重要。不過,有一個重要的驗證竅門現在有必要一提。如果您接收一個 cookie 值,檢查它的域值是不是您所期望的(比如,您的一個站點)。否則,一個(可能已經被擊垮)相關的站點可能被插入到用於欺騙的 cookie 中。如果您對此關心,可以參考 IETF RFC 2965 ,可以得到關於這種攻擊的詳細說明(在 參考資料 中有相關鏈接)。
HTML 有時候您的程序要從一個不信任的用戶處得到數據並把它傳給其他的用戶。如果第二個用戶的程序有可能被這些數據破壞掉,那麼您有責任保護第二個用戶。使用看起來可信任的中間媒介傳輸惡意數據的攻擊被稱為“交叉站點惡意內容”攻擊。
這個問題對於網絡應用來說尤其是一個難題,比如那些允許用戶添加當場連續評述的社區"黑板"。在這種情況下,攻擊者可以嘗試添加包含惡意代碼腳本、圖片標簽的 HTML 格式的評述;目的是讓其他用戶的浏覽器運行在察看本文的時候去執行那些惡意的代碼。由於攻擊者通常是試圖添加惡意的腳本,因些這種變化被稱為"交叉站點腳本攻擊"(XSS 攻擊)。
通常來說避免這種攻擊的最好的辦法是驗證您所接收的 HTML 沒有包括這種惡意的腳本。同樣,您要做的是把您所知道的安全的列出來,然後禁止其他。
通常,在 HTML 中您至少可以接收下面這些以及它們的結束標簽:
<p> (段落)
<b> (加粗)
<i> (斜體字)
<em> (強調)
<strong> (特別強調)
<pre> (預定義文本)
<br> (強制斷行 -- 注意它不需要關閉標簽)
記住 HTML 標簽是不區分大小寫的。除非您檢查過了屬性的類型和它的值,否則不要接收任何屬性;有很多支持 Javascript 等的屬性可能會給您的用戶帶來麻煩。
您當然可以擴展這個集合,但是要小心。特別需要注意的是任何讓用戶立即加載另一個文件的標簽,比如 image 標簽――那些標簽非常適合 XSS 攻擊。
另外一個問題是您需要確認攻擊者無法任意打亂文件的其余部分,特別是,您要確保任何評述或者片段看起來不能像正式的內容。一個方法是保證任何 XML 或 HTML 命令完全對稱(任何打開的都要關閉)。這在 XML 術語中稱為"格式良好的"數據。如果您正在接收標准 HTML,您可能不需要為段落標記(<p>)做這些,因為它們不是對稱的。
在許多情況下您可能要接收 <a> (超級鏈接),還可能需要屬性“href”。如果您必須這樣做,您必須要驗證您鏈接到的 URI/URL――這是我們的下一個話題。
URI/URL 技術上講,超文本鏈接可以是任何“統一資源標識符”(URI),不過現在大多數人只知道一種特定的 URI,那就是“統一資源定位符”(URL)。許多用戶將盲目地點擊指向一個 URI 的超級鏈接,並假定不會因為顯示它而帶來麻煩。作為一個開發者,您的任務是確保用戶的期望不會落空。
盡管 URI 提供了很大的靈活性,可是如果您接收到了一個來自攻擊者的 URI,您需要在把它轉給任何其他人之前檢查它。攻擊者可以在 URI 中加入很多古怪的東西來迷惑用戶。例如,攻擊者可以引入一些查詢,而導致用戶去做不願去做的事情,並且他們可以讓用戶誤以為是要浏覽另一個網站而不是他們確實在訪問的。 不幸的是,很難給出一個適用於所有情況的單一模式。不過,一個可以防止大多數攻擊,同時可以允許大多數有用的鏈接通過的(對於公共的網站而言)最安全的模式是:
^(http|ftp|https)://[-A-Za-z0-9._/]+$
一個更為復雜的模式是:
^(http|ftp|https)://[-A-Za-z0-9._]+(\/([A-Za-z0-9\-\_\.\!\~\*\'\(\)\%\?]+))*/?$
如果您的需要更復雜,您就需要更復雜的模式來檢查數據;可以在我的書中(在 參考資料 中列出了)查找一些其他方法。
數據文件 復雜的數據文件和數據結構通常由眾多的小的組件構成。只需要把這個文件或者結構分解,並檢查每一部分。如果這些組件之間的有特定的依賴關系,那就一並檢查它們。在開始時,編寫這些代碼可能會有一點無聊,不過它對於
可靠性確實有好處:如果您拒絕了那些非法數據,許多不可思議的問題馬上就會消失。
結束語 顯然,有很多種不同的數據需要去檢查。但是這些數據是在何處進入到您的程序的?答案在各種情況下不盡相同;實際上,您的程序可能會通過您所沒有想到的途徑得到攻擊者的數據。我將在下一部分討論這個問題。