作者:張傑生 / 臺灣大學計算機及資訊網路中心作業管理組
前言
隨著網際網路日漸普及,可以透過網路的服務也越來越多:從日常購物到銀行轉帳,甚至連違規罰單繳納也在服務之列。然而在使用這些便利服務的同時,卻不得不令人擔憂輸入的帳號密碼、信用卡號是否有可能遭到竊取,以及提供服務的網站的安全性是否值得信賴。身為網路服務提供者,如何提升網站安全性,保護使用者免於恐懼,乃是攸關服務提供者聲譽的首要任務。
根據SANS網站每年所公布的Top Security Vulnerabilities統計[1],我們可以發現過去幾年所發生的資訊安全事件,許多都是肇因於作業系統(Windows、Linux)本身,或者是網站伺服器(如IIS)程式的安全漏洞,造成駭客有機可乘,進而入侵破壞。然而經過無數次的挑戰與磨練,大部分能夠存活下來的軟體,都已經具備相當程度的安全等級。再加上Firewall與IDP等硬體式防護設備的盛行,因此近幾年所發生的資訊安全事件,反而多是網站本身的程式設計缺失所造成。由此可見,決定了網站未來對於惡意攻擊的防禦能力,取決於發展者是否具有良好的安全程式寫作觀念與技巧。
嚴格檢查所有使用者輸入資料(input validation)
幾乎90%以上的安全漏洞,包括SQL Injection與Cross Site Scripting等,都是來自於惡意使用者的輸入資料。為了滿足Web應用程式的互動式需求,我們不可能因噎廢食,禁止使用者輸入任何資料。因此我們唯一能夠採取的應對機制,只有嚴格檢查所有使用者的輸入資料,避免任何不合法惡意碼乘隙而入。
最常發生SQL Injection的頁面,通常位於使用者帳號密碼驗證或商品查詢功能,然而這也是最容易防堵的漏洞。一般來說,使用者的帳號、密碼,或者是商品的名稱,通常都會有合理長度限制,以及字元種類限制,舉例來說,帳號及商品名稱,通常不會出現A-Z、0-9以外的字元(當然中文的情況需另外處理)。因此,我們可以在伺服器端的程式碼,增加輸入欄位的長度檢查,一旦發現不合理的輸入資料,可以加以拒絕或者是僅接受部分長度。當然,更高規格的作法,需要利用regular expression進行最嚴格檢查,例如規定輸入資料必須符合 [A-Za-z0-9]{3,20} 的格式,如此一來,僅接受大小寫英文字母及數字的組合,且長度需介於3至20字元數之間。根據經驗,SQL Injection攻擊所需要的指令碼長度,通常遠超過合理輸入值,因此,只要完成上述長度與合法字元檢查,就可以阻決90%以上的SQL Injection攻擊。
更嚴格的方式是利用程式語言內建的字串檢查函式,將所有可能成為SQL指令參數的字串,預先過濾檢查,以求萬無一失。以PHP為例,常用的函式包括:addslashes()、pg_escape_string()、mysql_escape_string()等。
然而,防禦難度較高的情況,通常是無法限制資料長度及字元種類的輸入,例如:個人資料、留言版等。在這種情況之下,只能利用regular expression檢查並取代敏感的字元,例如: < > ‘ “ - 等。或者利用內建的函式,如PHP的htmlspecialchars(),將特殊字元轉換成HTML格式,例如:>變成>。甚至禁止使用者輸入網址連結、Java Script、ASP或PHP等程式碼,藉以降低被植入惡意程式碼的機會。
另外,如果程式碼涉及open、write file的情況,則務必仔細檢查使用者輸入的path與filename,例如禁止 .. / \ 等字元,以避免使用者惡意開啟或覆蓋不屬於授權範圍的檔案。
總結來說,網頁程式安全守則最重要的一條,就是絕對不可信任使用者的輸入資料,必須用放大鏡仔細檢視。而負責檢查的程式務必位於伺服器端,使用者端的javascript甚至程式碼裡面的FORM INPUT MAXLENGTH都很容易被使用者取消或者跳過,因此僅能視為輔助工具。簡單整理,如果要攔阻SQL Injection攻擊,則必須過濾SQL meta-characters,包括:’ – ; # 以及其hex code %27 %2D %3B %23。因為現在許多惡意程式都知道要將字元改以hex方式呈現,藉以逃避程式檢查。如果我們忽略一併檢查相對應的hex code,很容易功虧一簣。至於Cross Site Scripting的話,過濾 < > 以及其hex code即可應付大部分的攻擊行為。
嚴格控管使用者上傳檔案
允許使用者上傳檔案,往往開啟一扇安全漏洞大門,尤其是遇到使用者惡意上傳可執行檔或者ASP、PHP程式碼時。正確的作法是,將上傳檔案集中存放於某幾個目錄之下,並且透過網站程式(IIS、apache)的限制,禁止該目錄以下的程式執行。如此即便使用者嘗試利用瀏覽http://test.ntu.edu.tw/path/malicious.php的方式執行上傳檔案,頂多也只能看到檔案內容,不至於啟動該程式碼。當然還有一個簡單的方法,可以達到90%以上安全程度。也就是將所有上傳檔案的副檔名自動加上某一字串,例如 .txt或 .haha,如此一來伺服器就不會將之視為ASP或PHP之script檔案,也不會嘗試interpret及執行,可以算是一種短期的simple workaround。
額外的連線來源檢查
Cross Site Scripting的惡意應用之一,可以透過執行javascript,讀取cookie檔案,並將內容送至遠方網站儲存。未來有心人士則可利用該cookie冒充使用者身份,進行網站交易。為了阻止這類盜用事件,安全的網站程式,應該加入使用者來源IP address檢查。正常情況下,使用者在同一段時間內所使用的IP address並不會改變,因此一旦發現有其他IP address的連線嘗試以相同cookie存取網站,則很可能發生入侵行為。這時候網站程式應該當機立斷,將使用者logout,要求重新輸入帳號密碼,甚至直接拒絕連線一段時間。
除了IP address檢查之外,更進一步的保護措施,可藉由檢查網頁的Referer欄位達成。Referer欄位通常記錄著上一頁瀏覽網頁的連結。舉例來說,一個典型的學生資訊系統的瀏覽次序,應該是login.php -> query-grade.php -> change-address.php -> logout.php。那麼當瀏覽query-grade.php時,Referer欄位記錄的就是login.php。因此,真正機密敏感的網頁程式,可以透過檢查Referer欄位的方式,確認使用者瀏覽的順序步驟,是依照程式開發人員制訂的流程進行。換句話說,使用者在查詢成績時,理論上應該先通過帳號密碼驗證網頁,因此query-grade.php程式就可以限制Referer必須為login.php。
經過嚴格的Referer檢查之後,惡意人士就不容易藉由竊取的cookie資訊,跳過login.php直接進行成績查詢,窺探隱私。順帶一提,目前各大貼圖網站所流行的「防盜連」功能,大多就是利用Referer檢查的方式做到。
避免將敏感資訊直接寫於程式碼
網路上有許多免費的程式碼檢測(Code Review)工具,利用這些工具掃瞄網站程式碼後,通常可以得到許多程式修改建議。除了常見的宣告問題外,必定會被列為嚴重缺失的,就屬於將帳號密碼直接寫在程式碼內。一般程式設計人員,或許因為懶惰,或許因為習慣不良,不時都會發生類似狀況,其中又以資料庫連線的IP address及帳號密碼最常出現。未來一旦發生程式碼洩漏,則原本應該隱身於幕後的資料庫主機資訊立即曝光,有心人士即可嘗試直接存取資料庫,進而竊取及竄改重要資料。程式碼洩漏的原因有很多,除了網站遭入侵之外,有些時候如果伺服器啟動debug功能,而使用者故意迫使程式不正常中斷,伺服器就會列印出相關程式碼資訊。還有一種經常被忽略的情況,現在許多編輯器會自動將檔案備份,並且命名為filename.bak,有經驗的使用者知道可以嘗試存取http://test.ntu.edu.tw/login.php.bak,如果檔案確實存在,伺服器會將檔案內容完整輸出,於是敏感的程式碼就一覽無遺。
正確的程式寫作習慣,應該將這類設定資訊(configuration)獨立存放於另外的設定檔,如果未來需要更改資料庫主機,僅需修改該設定檔即可,無須逐一修改其他程式碼,如此可以提升程式的可移植性(portability)。以網頁程式而言,該設定檔最好能夠放置於www的目錄之外(c:\interpub\wwwroot或 /usr/local/www/htdoc),如果不幸程式碼洩漏,即便設定檔位置曝光,也無法利用簡單的 http://test.ntu.edu.tw/path/config.file 呼叫方式取得。
實體區隔一般使用者與管理者的網頁
為了進行某些特殊功能,資訊系統通常都會額外設計一套管理介面。以學生成績系統為例,除了老師輸入、學生查詢的介面之外,勢必還需要一套更高權限的管理介面,以處理老師輸入錯誤,或者是設定助教帳號等功能。然而, 我們總是會發現許多網站,竟然就在公共網頁主選單的某一項,明目張膽地加入「管理者專用」的連結。雖然點選之後一定會跳到帳號密碼驗證的畫面,可是我們認為,這就是一種「引誘犯罪」的行為。只要能夠挑起別人的好奇心,利用程式進行暴力法(brute force)破解密碼,成功之時指日可待。
有些比較有經驗的程式設計人員,會選擇隱藏管理介面的連結,避免讓人輕易得知。然而即便他們將管理介面隱身於其他子目錄,例如http://test.ntu.edu.tw/path2/admin.php,依舊很有可能遭到暴力法嘗試搜尋成功。更何況許多人習慣將目錄取名為admin、adm、internal、private這些早已well-known的pattern,恐怕只有他們自己相信安全無虞。
我們推薦的正確作法,應該要將一般使用者與管理者的網頁,放置於不同的實體機器,例如:http://test1.ntu.edu.tw/login.php與http://test2.ntu.edu.tw/path2/admin.php,此外兩者用來連結資料庫的帳號權限,也必須分別設定。可預見的好處包括,使用者猜破頭也找不到管理網頁的主機名稱、網址路徑,大幅降低網頁遭入侵的機會。即使一般使用者的網頁遭入侵,駭客也僅能取得低權限的資料庫帳號,例如只允許SELECT,危害程度有限。最後,如果系統要求的安全等級更高,我們可以在管理者網頁加上額外控管,例如帳號密碼認證,以及來源IP address限制,如此一來,幾乎可說是滴水不漏、萬無一失。
正式上線系統取消「顯示debug訊息」功能
在程式開發期間,為了除錯需要,往往會啟動系統的debug功能。之後程式發生runtime error時,系統就會自動將所有相關資訊列印在網頁上,以利後續問題追查。然而這些顯示的資訊,通常包含:程式碼錯誤原因、出錯程式碼行數以及程式碼片段,如果是資料庫問題,甚至會將資料庫錯誤回應訊息、主機位置、table名稱以及所執行的SQL指令通通顯示出來。由於資訊豐富,很容易遭到有心人士覬覦。
實務上來說,惡意使用者可以在網頁的各個輸入欄位,嘗試填入各種奇怪數值(例如年齡>200、採購數量為負值、參加人數不為正整數等)、各式特殊字元或者是超長字串,甚至發動DOS(Denial of Service)攻擊造成系統過載,期待因此造成程式碼parsing或execution錯誤,進而列印出寶貴的debug資訊。最後藉由拼湊資訊,找到系統弱點。舉例來說,一旦得知SQL指令的查詢方式,接下來就可以嘗試SQL Injection的可能性。
為了保障資訊安全,正式上線的production系統,建議取消所有的debug訊息顯示功能,杜絕任何可能造成資訊洩漏的機會。然而更周延的設計方法,應該要善用作業系統本身所提供的事件記錄功能,例如:syslog(),將所有debug、warning、notice訊息,集中存放於系統的記錄區(/var/log),或者是傳送到remote的log server,避免一般使用者存取系統記錄檔案。如此可以同時兼顧debug的方便性及系統之安全防護兩種考量。
結語
在可預見的未來數年內,WWW絕對是繼續穩居Internet服務的主流地位。當所有網站服務都開始強調個人化的同時,也代表著更多的使用者個人資訊會被記錄下來。如何取信於使用者,讓他們得以安心輸入資料,進行線上購物等網路交易,決定了網站日後的成長性。因此,如何規劃、建置、維運一個安全的網站服務,不但是成功的關鍵因素,也是當務之急。本文嘗試提出一些網頁程式設計的安全寫作建議,希望能夠分享筆者多年程式開發經驗,幫助新進程式設計人員,提升程式品質,強化程式安全性,共同為Information Security盡一份心力。
參考資料
[1] SANS Top-20 2007 Security Risk, http://www.sans.org/top20
[2] Firebug, http://www.getfirebug.com
[3] Fiddler HTTP Debugger, http://www.fiddlertool.com/fiddler
[4] SQL Injection, http://www.spidynamics.com/whitepapers/WhitepaperSQLInjection.pdf
[5] Cross Site Scripting, http://www.spidynamics.com/assets/documents/SPIcross-sitescripting.pdf