CGI使得互聯網上的任何人都可以在你的計算機上運行程序,這就使得CGI成為世界上最流行的安全漏洞。作為程序員,我們的責任是不讓壞人侵入我們的系統,對於我們所編制的程序來說,要做到沒有漏洞可鑽。 例如,下面這個CGI程序,就是個壞程序: #!/usr/bin/perl -w # cgi-bad – 一個不好的cgi 腳本的例子 $file = param("FILE") or die "Must fill out the FILE field\n"; unlink("/usr/local/public/data/$file") or die "Can delete $file : $!\n"; 該腳本所做的是讀出在表單中所輸入的文件名,並從目錄/usr/local/public/data/中刪除該名稱的文件。錯了!該腳本所做的實際上是讓任何用戶對在網絡服務器上usercode可以刪除的任何文件作刪除操作。請看: % setuid-bad ../../etc/apache/var/userdb 我們本來要做的是檢查程序的參數,以確定其是否為文件名。問題是你的程序外部所產生的數據用到了系統調用上,如nlink(), open(),和system()。而你並不打算讓在你的程序之外產生的數據影響到外部世界。 Perl有個選項,打開後,可以強迫你檢查常數,環境,輸入,或其它有可能被不懷好意的人利用的漏洞。該選項稱為“tainting”
打開Taint檢查選項 要打開taint檢查選項,讓Perl帶一個 -T 選項: #!/usr/bin/perl -wT 如果我們在上述程序運行時,帶有 –T選項,我們會看到如下信息: Insecure dependency in unlink while running with -T switch at setuid-bad line 5. Perl跟蹤$file中的值,它是在你的程序外部生成的,(它被稱為“tainted”)。 unlink() 被認為是個不安全的操作,因為它對外部世界有影響:文件。在不安全的操作下,企圖使用沒有信任度的(tainted)數據是危險的。正如我們已經看到的,數據可能有詐。 這些漏洞可以由Perl的taint檢查選項在運行時捕捉到,並且使得程序停下來。
Tainted數據 Tainted 數據來源很多,包括:來源於你的環境散列表 (the %ENV) ,參數 (@ARGV),讀入的文件和目錄,來源於運行的程序中,以及一些系統調用的結果(用getpw讀出口令數據庫中的GECOS域)。任何對tainted值的操作(添加,合並,插入),其結果值也是tainted。這就好像是數據一旦被粘上了污點,那麼無論數據傳播到哪裡,污點就會被帶到哪裡。 僅有三種方式,可以得到“untainted”值:數據直接在程序中指明;數據來自於安全的函數(如localtime);或者使用正則表達式提取來自不安全函數的tainted 串的一部分。 $a = 4; # untainted $file = $ARGV[0]; # tainted $file =~ m{^([^/]+)$} or die "$file is not a good filename.\n"; $untainted = $1; # untainted 通過正則表達式用括號括起來,創建了$1, $2, ... 變量。這些都是untainted數據。通過正則表達式,你可以確信它就是你所期望的值。如果匹配失敗,你會得到失敗信息。如果匹配成功,$1 ...變量包含了你可以使用的untainted 數據。 如果我們已經打開tainting 選項,當我們試圖做unlink()操作時,Perl 解釋器會停下來,告訴你$file 中包含了tainted 數據。文件名是 tainted的,因為它來自於不信任源:使用你的程序的人。
壞動作 如果你所使用的數據是tainted的,你想要Perl程序所做的大多數事情會產生出錯信息。如果文件名或程序名是tainted的,那麼運行程序,打開文件來寫入,以及刪除文件,這些操作都將被禁止進行。 這一節將演示如何在這種場合下,解除tainted狀態。 考慮: system("ls *.h"); Perl 在你的串中看到了 *,並決定調用shell,這樣: sh -c "ls *.h" 但是,的確有人可能用假的路徑環境變量來運行你的程序,從而導致調用了錯誤的sh或ls。所以,對於PATH變量以及SHELL中可以用來修改其行為的其他變量,應該進行 untaint操作。 一般,運行其它程序時,你應采取三項步驟: 明確你的環境變量,使得運行的是實際程序。
關閉shell 對程序的參數進行untaint操作。 用如下的等簡單方式清除你的環境變量: delete @ENV{"IFS", "CDPATH", "ENV", "BASH_ENV"}; $ENV{PATH} = "/bin:/usr/bin"; 第一行刪除掉可能會引起問題的環境變量,第二行給出一個確保安全的PATH。你可以添加其他的目錄到PATH中,但務必確保它們同該處一樣,是有確定值的。 關閉shell也要把握好分寸。Perl 在涉及到有關shell的操作,如 open(), system(), backticks,和exec() 調用時,有自己的規則,這些規則不太容易掌握。最好的規則是:避免使用backticks 和pipe open() 調用,而是使用system() 和exec() ,並傳給它們參數表。 大多數人習慣於看到如下的寫法: system("someprogram arg1 arg2 arg3"); 他們不知道還可這樣寫: system("someprogram", "arg1", "arg2", "arg3"); 這樣的寫法,可以精確地告訴Perl的各個參數是什麼,Perl將不會調用shell。 exec() 也具有讀參數表和不調用shell的特點。而如果要使用piped open() 和backticks,就無法保證不會用到shell。 如果你打算使用piped open 或 backticks,你得用如下的方法重新實現: $pid = open(COMMAND, "-"); die "Couldn fork: $!" unless defined $pid; if ($pid) { @lines = ; close(COMMAND); } else { exec("some", "program", "with", "args") or die "execing: $!"; } 一般來說,即使你的PATH已經作了安全處理,給出所運行的程序的完整路徑是個好主意。這就會避免了錯誤地調用了/usr/bin/boom 而不是/home/user/bin/boom這種情況的發生,因為在PATH中 /usr/bin 位於/home/usr/bin/boom.之前。
文件名 對文件名進行操作時,使用unlink() 或 ,或者用open()時,是有危險的。 從目錄中讀入的文件名是tainted的。你可以打開一個tainted 文件名來讀入,但你不能打開它來寫入。從文件中讀數據,不管文件名是否 tainted,已經是tainted的。因為用到了shell,你不能用 來得到文件清單。 為了檢查文件名是否是好的,你得寫出一個正則表達式,並同合法的文件名進行匹配。在一些場合,可以用如下的簡單方法來檢查你的數據: $file = $ARGV[0]; ($file =~ m{^([^/]+)$} && $file ne "." && $file ne "..") or die "Bad filename $file\n"; $file = $1; 根據任何不包含斜槓的串的正則表達式來檢查文件名,這就把子目錄排除在外,然後排除掉“.”(當前目錄)和“..”(當前目錄的父目錄)。如果這些測試都通過了,$1變量中存放的就是我們可以使用的文件名。 為了得到匹配某種模式的文件名清單,你既可以從CPAN (File::KGlob 和File::BSD 是兩個有用的模塊)安裝有關模塊,也可以使用讀目錄操作和正則表達式:
Untainting過了頭也會有問題 在不多的場合,盲目地untaint你的數據也產生安全漏洞。所以也此時需要Tainting的存在。如果象下面一樣,盲目地對任何數據都untaint: $var =~ /(.*)/s; # 愚蠢 $var = $1; 正則表達式中的 /s 符號使得句點可以匹配串中的任何換行符。 通過用 .* 我們匹配了串中的一切符號,並用$1存放該數據的untainted的副本。 正如注釋所說的,這樣做是愚蠢的。
總結 -T 打開tainting選項。來自你程序之外的數據是tainted,不能使用這些數據,以免影響外部世界。 用正則表達式和$1, $2, ... 變量進行untaint。要運行其他程序,設置好path,不要使用shell,並對參數進行untaint。
進一步的閱讀 在perlsec manpage 中詳細闡述了tainting的機制,並給出了較多的例子。Chapter Perl Cookbook的第十六章談了進程管理,演示了non-shell 版的 piped opens和其他有趣的用法