CGI教學(xué):CGI安全問(wèn)題(再續(xù))
發(fā)布時(shí)間:2008-08-06 閱讀數(shù): 次 來(lái)源:網(wǎng)樂(lè)原科技
2.7 處理文件名
文件名是提交給CGI腳本的簡(jiǎn)單數(shù)據(jù),但如果不小心的話,卻能導(dǎo)致許多麻煩。如果用戶輸入的名字中包含路徑因素,如目錄斜杠和雙點(diǎn),盡管期望的是輸入一個(gè)簡(jiǎn)單的文件名--例如file.txt--但結(jié)果卻可能是/file.txt或../../../file.txt。根據(jù)Web服務(wù)器的安裝以及對(duì)提交的文件名做什么操作,系統(tǒng)中的所有文件就有可能都暴露給了一個(gè)聰明的黑客。
進(jìn)一步,如果用戶輸入了一個(gè)已有文件的名字或者一個(gè)對(duì)系統(tǒng)的運(yùn)行很重要的文件名,怎么辦?對(duì)如果輸入的名字是/etc/passwd或C:\WINNT\SYSTEM32\KRNL32.DLL怎么辦?根據(jù)在CGI腳本中對(duì)這些文件進(jìn)行什么操作,它們有可能被發(fā)送給用戶或者被垃圾覆蓋了。在Windows 95和Windows NT下,如果不檢查反斜杠字符(\),可能會(huì)允許Web 瀏覽器通過(guò)UNC文件名訪問(wèn)甚至不在該Web機(jī)器上的文件。
如果用戶在文件名中輸入了不合法的字符怎么辦?在UNIX下,任何以句點(diǎn)(.)開頭的文件名都是不可見(jiàn)的。在Windows下斜杠(/)和反斜杠(\)都是目錄分隔符。很可能不小心寫了一個(gè)Perl程序,當(dāng)文件名以管(pipe)(|)開頭時(shí),盡管自己以為僅僅是打開了一個(gè)文件,實(shí)際上卻是執(zhí)行了一個(gè)外部程序。如果用戶知道怎么辦的話,甚至可以把控制字符(例如Escape鍵或Return鍵)作為文件名的一部分送給腳本。
更壞的情況是,在shell腳本中,分號(hào)用于結(jié)束一條命令并開始另一條命令。如果腳本設(shè)計(jì)目的是cat用戶輸入的文件,用戶可能輸入file.txt;rm-rf/作為文件名,導(dǎo)致返回fi1e.txt,然后清除整個(gè)硬盤而不經(jīng)任何確認(rèn)。
2.8 輸入合理,輸出卻不合理
為了避免所有這些問(wèn)題,關(guān)閉由它們打開的所有安全縫隙,檢查用戶輸入的每個(gè)文件名。必須確保輸入正是程序預(yù)期的輸入。
這樣做的最好辦法是將輸入的文件名的每個(gè)字符與可接收字符的清單進(jìn)行比較,如果不匹配就返回一個(gè)錯(cuò)誤。這比維持一個(gè)所有合法字符的清單并比較它們要安全得多——要想讓什么字符溜掉太容易了。
以下程序清單是用Perl如何完成這種比較的例子。它允許任何字符字母(大寫或小寫調(diào))、任何數(shù)字、下劃線和句點(diǎn)。它還進(jìn)行檢查以確保文件名不以句點(diǎn)開頭。這樣,該段代碼就不允許可以改變目錄的斜杠,不允許可以將多條命令放在一行的分號(hào),或者破壞Perl的Open()調(diào)用的Pipes了。
程序清單 保證所有字符都是合法的
if (($file_Name =~ /[^a-zA-Z_\.]/) || ($file_Name =~ /^\./)) {
#File name contains an illegal characgter or starts with a period
}
警告
盡管上述程序清單中的代碼清除了大部分不合法的文件名,但操作系可能還有一些限制,而該代碼沒(méi)有覆蓋到。例如,文件名可以用數(shù)字開頭嗎?或者以下劃線開頭?如果文件中包含多個(gè)句點(diǎn)或者句點(diǎn)后多于三個(gè)字符怎么辦?整個(gè)文件名足夠短得能滿足文件系統(tǒng)的限制嗎?
必須不斷向自己提出這種問(wèn)題。在寫CGI腳本時(shí)最危險(xiǎn)的事是認(rèn)為用戶會(huì)遵守指令。其實(shí)用戶是不會(huì)的。保證用戶不犯錯(cuò)誤是編程者自己的事。
2.9 處理HTML
另外一種看起來(lái)無(wú)害的但卻能導(dǎo)致很大麻煩的輸入是在請(qǐng)求用戶輸入文本信息時(shí)得到的HTML。以下的程序清單是一個(gè)Perl程序片段;它向任何在$user_Name變量中輸入了一個(gè)名字的人,例如John Smith,發(fā)出問(wèn)候信息。
程序清單 發(fā)出定制的問(wèn)候腳本
print ("<HTML><TITLE>Greetings!<TITLE><BODY>\n");
print ("Hello,$user_Name! It's good to see you!\n");
print ("</BODY><HTML>\n");
想像一下,如果用戶不是僅僅輸入一個(gè)名字,而是輸入了<HR><H1><P ALIGN="CENTER">John Smith</P><H1><HR>或想像一下當(dāng)腳本希望得到用戶名時(shí),黑客輸入了<IMG SRC="/secret/cutekid.gif">,結(jié)果是公開了本該保密的信息。允許輸入HTML可能很危險(xiǎn)。
比輸入簡(jiǎn)單的HTML修改頁(yè)面或訪問(wèn)畫面更危險(xiǎn)的是惡意的黑客可能輸入一條服務(wù)器端的include指令。如果web服務(wù)器設(shè)置為服從服務(wù)器端include,用戶就可以輸入
<!--#include file="/secret/project/p1an.txt"-->
而不是他的名字,以便看到秘密計(jì)劃的全部文本,或者用戶可以輸入<!--#inc1ude fi1e-"/etc/passwd"-->來(lái)獲取機(jī)器的口令文件。可能最壞的情況是黑客可能輸入<!--#exec cmd="rm-rf/"-->而不是他的名字。這樣上述程序清單中的代碼會(huì)刪掉硬盤上幾乎所有內(nèi)容。
警告
由于經(jīng)常被惡意地使用,服務(wù)器端的include經(jīng)常被禁止使用以保護(hù)站點(diǎn)免受侵害。現(xiàn)在假定這些都沒(méi)問(wèn)題。即使關(guān)閉了服務(wù)器端的include并且不介意用戶能看到自己硬盤上的任何圖片或者改變頁(yè)面顯示的外觀,也仍然有問(wèn)題--不僅是針對(duì)編程者的,而且針對(duì)其他用戶。
CGI腳本的一個(gè)通常用途是留名冊(cè)(guestbook):訪問(wèn)站點(diǎn)的顧客可能簽個(gè)名,讓別人知道他們已經(jīng)在那兒了。一般情況下用戶簡(jiǎn)單地輸入他的名字,該名字會(huì)在訪問(wèn)者清單中出現(xiàn)。但是,如果將The last signee!<FORM><SELECT>作為用戶名輸入怎么辦?<SELECT>標(biāo)記將導(dǎo)致Web瀏覽器忽略位于<SELECT>和一個(gè)不存在的</SELECT>之間的所有內(nèi)容,包括以后清單中加入的任何名字。即使有10個(gè)人簽了名,僅有前3個(gè)會(huì)顯示出來(lái),因?yàn)榈谌齻€(gè)名字包含一個(gè)<FORM>和一個(gè)<SELECT>標(biāo)記。因?yàn)榈谌齻€(gè)簽名者在他的名字中使用了HTML標(biāo)記,他后面的任何名字都不會(huì)顯示出來(lái)。
對(duì)于用戶輸入HTML而不是普通的文本的情況有兩種解決辦法:
1)快速但比較粗糙的辦法是不允許小于號(hào)(<)和大于號(hào)(>),因?yàn)樗蠬TML標(biāo)記必須包含在這兩個(gè)字符中,所以清除它們(或者如果碰到它們就返回一個(gè)錯(cuò)誤)是一種防止HTML被提交并返回的簡(jiǎn)單的辦法。下面一行Perl代碼簡(jiǎn)單地清除了這兩個(gè)字符:$user_Input=~s/<>//g;
2)更精細(xì)一點(diǎn)的辦法是將這兩個(gè)字符轉(zhuǎn)換成它們的HTML換碼--—種特殊的代碼,用于表示每個(gè)字符而不使用該字符本身。下面的代碼通過(guò)全部用<替換了小于符號(hào),用>替換了大于符號(hào),從而完成了轉(zhuǎn)換:
$user_Input=~s/</&1t;/g;
$user_Input=~s/>/>/g;
.10 處理外部進(jìn)程
最后,CGI腳本如何與帶有外部過(guò)程的用戶輸入打交道是應(yīng)該警惕的另一區(qū)域。因?yàn)閳?zhí)行一個(gè)位于自己的CGI腳本之外的程序意味著無(wú)法控制它做什么,必須盡最大努力在執(zhí)行開始前驗(yàn)證發(fā)送給它的輸入。
例如,shell腳本經(jīng)常錯(cuò)誤地將一個(gè)命令行程序和表單輸入合在一起執(zhí)行。如果用戶輸入符合要求,一切都挺正常,但是有可能會(huì)加入其它命令并非法執(zhí)行。
下面即是一個(gè)產(chǎn)生了這種錯(cuò)誤的腳本的例子:
FINGER_OUTPUT='finger$USER_INPUT'
echo $FINGER_OUTPUT
如果用戶很禮貌地給finger輸入了某人的e-mail地址,一切都會(huì)正常工作,但是如果他輸入了一個(gè)e-mail地址,后面再跟一個(gè)分號(hào)和另一條命令,那么該命令也會(huì)被執(zhí)行,如果用戶輸入了webmaster@www.server.com;rm-rf/,那麻煩可就大了。
即使沒(méi)有什么隱藏的命令被加入用戶數(shù)據(jù),無(wú)意的輸入錯(cuò)誤也可能帶來(lái)麻煩。例如,下面的代碼行會(huì)產(chǎn)生一個(gè)意料之外的結(jié)果——列出目錄中的所有文件——如果用戶輸入是一個(gè)星號(hào)的話。
echo "Your input:"$USER_INPUT
當(dāng)通過(guò)shell發(fā)送用戶數(shù)據(jù)時(shí),就象前面的代碼片段所做的那樣,最好檢查一下shell的meta-character(元字符)——這些可能會(huì)導(dǎo)致意外的行為。
這些字符包括分號(hào)(允許一行中有多條命令),星號(hào)和問(wèn)號(hào)(完成文件匹配),感嘆號(hào)(在csh下指運(yùn)行的作業(yè)),單引號(hào)(執(zhí)行一條包含其中的命令)等等。就像過(guò)濾文件名一樣,維護(hù)一個(gè)允許的字符清單一般要比試圖找出每個(gè)不允許的字符容易一些。下面的Perl代碼片段驗(yàn)證一個(gè)e-mail地址:
if ($email_Address ~= /[^a-zA-z0-9_\-\+\@\.]) {
#lllegal character! }
else { system("finger $email_Address"); }
如果決定在輸入中允許shell元字符,也有辦法讓它們安全一些。盡管可以簡(jiǎn)單地給未驗(yàn)證的用戶輸入加上引號(hào)以免shell按特殊字符進(jìn)行操作,但這實(shí)際上不起什么作用。請(qǐng)看下的語(yǔ)句:
echo"Finger information:<HR><PRE>"
finger"$USER_INPUT
echo"</PRE>
盡管$USER_INPUT上的引號(hào)可以使shell不再解釋一個(gè)分號(hào),從而不允許黑客簡(jiǎn)單地插進(jìn)來(lái)一條命令,但該腳本仍有許多安全方面的漏洞。例如,輸入可能是'rm-rf/',其中單引號(hào)可以導(dǎo)致甚至在finger不知道的情況下執(zhí)行黑客的命令。
一種處理特殊字符的較好的辦法是對(duì)它們進(jìn)行換碼,這樣腳本只是取它們的值而不解釋它們。通過(guò)對(duì)用戶輸入進(jìn)行換碼,所有的shell元字符都被忽略并作為增加的數(shù)據(jù)傳給程序。下面的Perl代碼即對(duì)非字母數(shù)字字符完成這種處理。
$user_Input=~s/([^w])/\\\1/g;
現(xiàn)在,如果用戶輸入加在某條命令之后,每個(gè)字符——即便是特殊字符——都會(huì)由shell傳送給finger。
不過(guò)請(qǐng)記住,驗(yàn)證用戶輸入——不相信發(fā)送給自己的任何信息——會(huì)使自己的代碼更易讀并且執(zhí)行起來(lái)更安全。最好不是在已經(jīng)執(zhí)行了命令之后再去對(duì)付黑客,而應(yīng)在門口就對(duì)數(shù)據(jù)進(jìn)行一次性的檢查。
--------------------------------------------
處理內(nèi)部函數(shù)
對(duì)于解釋型語(yǔ)言,例如Shell和Perl,如果用戶輸入的數(shù)據(jù)不正確的話,有可能導(dǎo)致程序生成本來(lái)沒(méi)有的錯(cuò)誤。如果用戶數(shù)據(jù)被解釋為一部分執(zhí)行代碼,用戶輸入的任何內(nèi)容都必須符合語(yǔ)言的規(guī)則,否則就會(huì)出錯(cuò)。
例如,下面的Perl代碼片段也許會(huì)正常工作也許會(huì)產(chǎn)生錯(cuò)誤,這取決于用戶輸入的是什么:
if ($search_Text =~ /$user_Pattern/) {
#Match! }
如果$user_Pattern是一個(gè)正確的表達(dá)式,一切都會(huì)正常,但是如果$user_Pattern不合法;Perl就會(huì)失敗,導(dǎo)致CGI程序失敗——這可能是一種不安全的方式。為了避免這種情況,在Perl中至少應(yīng)有eval()操作符,它計(jì)算表達(dá)式的值并與執(zhí)行它無(wú)關(guān),返回一個(gè)碼值表示表達(dá)式是有效的還是無(wú)效的。下面的代碼即是前面代碼的改進(jìn)版。
if (eval{$search_Text =~ /$user_Pattern/}) {
if ($search_Text =~ /$user_Pattern/) {
#Match!
}
}
不幸的是,大部分shells(包括最常用的,/bin/sh)都沒(méi)有像這樣的簡(jiǎn)單的辦法檢查錯(cuò)誤,這也是避免它們的另一原因。
--------------------------------------------
在執(zhí)行外部程序時(shí),還必須知道傳送給那些程序的用戶輸入是如何影響程序的。編程者可以保護(hù)自己CGI腳本不受黑客侵犯,但是如果輕率地將某個(gè)黑客輸入的內(nèi)容傳送給了外部程序而不知道那些程序是如何使用這些數(shù)據(jù)的,也會(huì)徒勞無(wú)益。
例如,許多CGI腳本會(huì)執(zhí)行mail程序給某人發(fā)送一個(gè)包含用戶輸入信息的e-mail。這可能會(huì)非常危險(xiǎn),因?yàn)閙ail有許多內(nèi)部命令,任何一個(gè)命令都有可能被用戶輸入激活。例如,如果用mail發(fā)送用戶輸入的文本而該文本有一行以代字號(hào)(~)開頭,mail會(huì)將該行的下一字符解釋為它能執(zhí)行的許多命令之一。例如,~r/etc/passwd,會(huì)導(dǎo)致mail讀取機(jī)器的口令文件并發(fā)送給收信人(也許正是黑客自己)。
在這樣的例子中,應(yīng)該使用sendmail(一個(gè)更底層的郵寄程序,它少了許多mail的特性),而不是使用mail在UNIX機(jī)器上發(fā)送e-mail。
作為一般規(guī)則,在執(zhí)行外部程序時(shí)應(yīng)該使用盡可能貼近自己要求的程序,不必有過(guò)多不必要的功能。外部程序能干的事越少,它被利用來(lái)干壞事的機(jī)會(huì)就越少。
警告
下面是使用mail和sendmail的另一個(gè)問(wèn)題:必須保證發(fā)送給mail系統(tǒng)的是一個(gè)合法的e-mail地址。許多mail系統(tǒng)都會(huì)把以"|"開頭的e-mail地址作為要執(zhí)行的命令,從而為輸入這樣一個(gè)地址的黑客打開方便之門,請(qǐng)?jiān)僖淮斡涀∫?yàn)證數(shù)據(jù)。
怎樣才能更好地了解外部程序以便有效地使用它們的另一個(gè)例子是grep。grep是一個(gè)簡(jiǎn)單的命令行實(shí)用程序,它在文件中搜索一個(gè)常用表達(dá)式,表達(dá)式可以是一個(gè)簡(jiǎn)單的串也可以是復(fù)雜的字符序列。大部分人會(huì)說(shuō)使用grep不會(huì)出什么問(wèn)題,但是盡管grep可能不會(huì)造成什么損失,它卻能被愚弄,下面將說(shuō)明它是怎么被愚弄的,如下面的代碼所示。它假定在許多文件中完成對(duì)用戶輸入項(xiàng)的區(qū)分大小寫的搜索。
print("The following lines contain your term:<HR><PRE>");
$search_Term=~s/([^w])/\\\1/g;
system("grep $search_Term/public/files/*.txt");
print(<"PRE>");
這一切看起來(lái)挺好,除非考慮到用戶可能會(huì)輸入-i。它不會(huì)被搜索,而是作為與grep的切換,就像任何以連字符開頭的輸入一樣。這會(huì)導(dǎo)致grep或者因等待將搜索的串輸入標(biāo)準(zhǔn)輸入而掛起,或者如果-i后的內(nèi)容被解釋為其他切換字符時(shí)產(chǎn)生錯(cuò)誤。毫無(wú)疑問(wèn)這不是編程者本來(lái)的意圖。在這種情況下它還不太危險(xiǎn),但在其他情況下卻有可能。記住,沒(méi)有什么無(wú)害的命令,對(duì)每條命令部必須從各個(gè)角度仔細(xì)考慮。
一般情況下,應(yīng)該盡可能熟悉自己的CGI腳本執(zhí)行的每個(gè)外部程序。對(duì)程序知道得越多,就越能保護(hù)它們免受數(shù)據(jù)破壞--一方面可以監(jiān)視數(shù)據(jù),另一方面可以禁止某些選項(xiàng)或特性。外部程序經(jīng)常是許多CGI程序問(wèn)題的一種快速方便的解決辦法——它們都經(jīng)過(guò)了測(cè)試,可以得到,并且靈活多樣。但它們也可以成為黑客入侵的方便之門。不要害怕使用外部程序——它們經(jīng)常是完成CGI程序中某種功能的唯一辦法——但是要知道它們可能帶來(lái)的危害。