目錄

名稱

perlsec - Perl 安全性

描述

Perl 設計為即使在執行具有額外權限的程式(例如 setuid 或 setgid 程式)時,也能輕鬆地進行安全編程。與大多數基於對腳本每行進行多次替換的命令行外殼不同,Perl 使用更傳統的評估方案,隱藏問題較少。此外,由於語言具有更多的內建功能,因此它可以較少地依賴外部(可能不可信)程式來完成其目的。

安全漏洞聯絡資訊

如果您認為您在 Perl 解譯器或核心 Perl 代碼庫中維護的模組中發現了安全漏洞,請將詳細信息發送至 perl-security@perl.org。此地址是一個由 Perl 安全團隊監控的封閉成員郵件列表。

請參閱 perlsecpolicy 以獲取其他信息。

安全機制和注意事項

污染模式

預設情況下,Perl 在偵測到程式以不同的實際使用者或群組 ID 執行時,會自動啟用一組特殊的安全檢查,稱為 污點模式。在 Unix 權限中,setuid 位元是模式 04000,setgid 位元是模式 02000;兩者都可以設定。您也可以透過使用 -T 命令列旗標來明確啟用污點模式。對於伺服器程式和代表其他人執行的任何程式(例如 CGI 腳本),強烈建議使用此旗標。一旦啟用了污點模式,它將在剩餘的腳本執行期間保持啟用狀態。

在此模式下,Perl 採取特殊預防措施,稱為 污點檢查,以防止明顯和微妙的陷阱。其中一些檢查相當簡單,例如驗證路徑目錄是否不可被其他人寫入;謹慎的程式設計人員一直以來都在使用這些檢查。然而,其他檢查最好由語言本身支援,尤其是這些檢查對於使 Perl 程式更安全的相應 C 程式而言。

您可能不會使用來自程式外部衍生的資料來影響程式外部的其他事物─至少不是意外地。所有命令列參數、環境變數、地域訊息(參見 perllocale)、某些系統呼叫的結果(readdir()readlink()shmread() 的變數、msgrcv() 返回的訊息、getpwxxx() 呼叫返回的密碼、gcos 和 shell 欄位),以及所有檔案輸入都被標記為 "污點"。污點資料不得直接或間接地用於調用子殼程式的任何命令,也不得用於修改檔案、目錄或處理程序的任何命令,但有以下例外情況

對污點檢查的支援會給所有 Perl 程式增加開銷,無論您是否使用污點功能。Perl 5.18 引入了可以用來停用污點功能的 C 預處理器符號。

出於效率考量,Perl 對於資料是否受污染採取保守態度。如果一個運算式包含了受污染的資料,則任何子運算式都可能被視為受污染,即使子運算式的值本身未受到受污染資料的影響。

由於污染是與每個純量值相關聯的,陣列或雜湊的某些元素可能受污染,而其他元素則未受污染。雜湊的鍵永遠不會受到污染。

例如

    $arg = shift;		# $arg is tainted
    $hid = $arg . 'bar';	# $hid is also tainted
    $line = <>;			# Tainted
    $line = <STDIN>;		# Also tainted
    open FOO, "/home/me/bar" or die $!;
    $line = <FOO>;		# Still tainted
    $path = $ENV{'PATH'};	# Tainted, but see below
    $data = 'abc';		# Not tainted

    system "echo $arg";		# Insecure
    system "/bin/echo", $arg;	# Considered insecure
				# (Perl doesn't know about /bin/echo)
    system "echo $hid";		# Insecure
    system "echo $data";	# Insecure until PATH set

    $path = $ENV{'PATH'};	# $path now tainted

    $ENV{'PATH'} = '/bin:/usr/bin';
    delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};

    $path = $ENV{'PATH'};	# $path now NOT tainted
    system "echo $data";	# Is secure now!

    open(FOO, "< $arg");	# OK - read-only file
    open(FOO, "> $arg"); 	# Not OK - trying to write

    open(FOO,"echo $arg|");	# Not OK
    open(FOO,"-|")
	or exec 'echo', $arg;	# Also not OK

    $shout = `echo $arg`;	# Insecure, $shout now tainted

    unlink $data, $arg;		# Insecure
    umask $arg;			# Insecure

    exec "echo $arg";		# Insecure
    exec "echo", $arg;		# Insecure
    exec "sh", '-c', $arg;	# Very insecure!

    @files = <*.c>;		# insecure (uses readdir() or similar)
    @files = glob('*.c');	# insecure (uses readdir() or similar)

    # In either case, the results of glob are tainted, since the list of
    # filenames comes from outside of the program.

    $bad = ($arg, 23);		# $bad will be tainted
    $arg, `true`;		# Insecure (although it isn't really)

如果您試圖執行不安全的操作,將會收到一個致命錯誤,類似於"Insecure dependency"或"Insecure $ENV{PATH}"。

在「一個受污染值會污染整個運算式」原則之外的例外情況是三元條件運算子?:。由於具有三元條件的程式碼

$result = $tainted_value ? "Untainted" : "Also untainted";

實際上是

if ( $tainted_value ) {
    $result = "Untainted";
} else {
    $result = "Also untainted";
}

$result受到污染是毫無意義的。

清洗和檢測受污染的資料

要測試變數是否包含受污染的資料,並且使用該變數將觸發「不安全相依性」訊息,您可以使用Scalar::Util模組的tainted()函數,該函數可在附近的CPAN鏡像中使用,並且在Perl 5.8.0版本開始提供。或者您可以使用以下的is_tainted()函數。

sub is_tainted {
    local $@;   # Don't pollute caller's value.
    return ! eval { eval("#" . substr(join("", @_), 0, 0)); 1 };
}

此函數利用了表達式中的任何地方存在受污染資料會使整個表達式變為受污染的事實。對於每個運算子都測試每個引數是否受污染是低效的。相反,採用了稍微更有效且保守的方法,即如果在相同的表達式中訪問了任何受污染的值,則整個表達式都被視為受污染。

但僅測試是否受污染並不能解決所有問題。有時您必須清除資料的污染。通常情況下,可以通過將其用作雜湊中的鍵來清除值的污染;否則,唯一繞過污染機制的方法是從正則表達式匹配中引用子模式。Perl 假定如果您在非受污染的模式中使用 $1、$2 等引用子串,那麼您在撰寫該模式時知道自己在做什麼。這意味著需要一點思考——不要盲目地清除任何東西,否則您會破壞整個機制。最好的方法是驗證變數僅包含良好字符(對於某些"好"值而言),而不是檢查它是否包含任何壞字符。這是因為很容易忽略您從未考慮過的壞字符。

這是一個測試,以確保資料只包含「字」字符(字母、數字和底線)、一個連字號、一個 at 符號或一個句點。

    if ($data =~ /^([-\@\w.]+)$/) {
	$data = $1; 			# $data now untainted
    } else {
	die "Bad data in '$data'"; 	# log this somewhere
    }

這相當安全,因為 /\w+/ 通常不匹配 shell 元字符,而點、破折號或 at 符號對 shell 不會有特殊含義。使用 /.+/ 在理論上是不安全的,因為它讓所有內容通過,但 Perl 不會檢查這一點。教訓是,在清理資料時,必須非常小心使用您的模式。使用正則表達式清理資料是唯一的消除髒資料的機制,除非您使用下面詳細介紹的策略來派生一個權限較低的子程序。

如果啟用了 use locale,則該示例不會對 $data 進行清理,因為由 \w 匹配的字符由語言環境確定。Perl 認為語言環境定義是不可信的,因為它們包含程序外的資料。如果您正在編寫具有語言環境意識的程式,並且希望使用包含 \w 的正則表達式來清理資料,請在同一個區塊中的表達式之前加上 no locale。有關更多討論和示例,請參見"SECURITY" in perllocale

在 "#!" 行上的開關

當您將一個腳本設置為可執行以便將其用作命令時,系統將從腳本的 #! 行傳遞開關給 perl。Perl 檢查任何傳遞給 setuid(或 setgid)腳本的命令行開關是否實際上與 #! 行上設置的開關匹配。一些 Unix 和類 Unix 環境對 #! 行施加一個開關限制,因此在此類系統中,您可能需要使用像 -wU 這樣的東西,而不是 -w -U。 (這個問題應該只在支持 #! 和 setuid 或 setgid 腳本的 Unix 或類 Unix 環境中出現。)

污點模式和 @INC

+當污點模式(-T)生效時,Perl 將忽略環境變數+PERL5LIBPERLLIBPERL_USE_UNSAFE_INC。您仍然可以通過使用 -I 命令行選項在程序外部調整 @INC,如 perlrun 中所述。兩個環境變數被忽略,因為它們被掩蔽,而運行程序的用戶可能沒有設置它們的意識,而 -I 選項則是明顯可見的,因此被允許。

另一種修改 @INC 而不修改程序的方法是使用 lib 声明,例如

perl -Mlib=/foo program

使用 -Mlib=/foo 而不是 -I/foo 的好處在於前者會自動刪除任何重複的目錄,而後者則不會。

請注意,如果將一個污點字符串添加到 @INC 中,將報告以下問題

Insecure dependency in require while running with -T switch

在 Perl 5.26 之前的版本中,啟用污染模式也會將當前目錄 (".") 從 @INC 的默認值中移除。自從 5.26 版本以來,當前目錄不再默認包含在 @INC 中。

整理您的路徑

對於 "不安全的 $ENV{PATH}" 消息,您需要將 $ENV{'PATH'} 設置為已知值,並且路徑中的每個目錄必須是絕對的,並且除了擁有者和組之外,其他人不能對其進行寫入。即使您的可執行文件的路徑名是完全限定的,您也可能會對此消息感到驚訝。這不是因為您沒有向程序提供完整的路徑,而是因為您從未設置過您的 PATH 環境變量,或者您沒有將其設置為安全的值。因為 Perl 不能保證所討論的可執行文件本身不會反過來執行依賴於您的 PATH 的其他程序,所以它確保您設置了 PATH。

PATH 不是唯一可能引起問題的環境變量。因為某些 shell 可能會使用變量 IFS、CDPATH、ENV 和 BASH_ENV,Perl 檢查在啟動子進程時這些變量是空的或未被污染的。您可能希望將類似這樣的代碼添加到您的 setid 和污染檢查腳本中。

delete @ENV{qw(IFS CDPATH ENV BASH_ENV)};   # Make %ENV safer

還有其他不關心是否使用污染值的操作也可能引起問題。在處理任何用戶提供的文件名時,請謹慎使用文件測試。在可能的情況下,在適當地放棄任何特殊用戶(或組!)特權之後再進行打開等操作。Perl 不會阻止您打開用於讀取的污染文件名,因此請小心打印出什麼。污染機制旨在防止愚蠢的錯誤,而不是消除思考的必要性。

當您傳遞系統和執行的顯式參數列表而不是帶有可能包含shell通配符的字符串時,Perl 不會調用 shell 來展開通配符。不幸的是,open、glob 和反引號函數沒有提供這種替代調用慣例,因此需要更多的詭計。

Perl 提供了一種相對安全的方法來從 setuid 或 setgid 程序中打開文件或管道:只需創建一個子進程,該子進程以降低的特權執行體系結構,為您完成骯髒的工作。首先,使用特殊的 open 語法 fork 一個子進程,該子進程通過一個管道將父進程和子進程連接起來。現在,子進程重置其 ID 集和任何其他逐進程屬性,例如環境變量、umasks、當前工作目錄,恢復到原始的或已知安全的值。然後,不再具有任何特殊權限的子進程執行打開或其他系統調用。最後,子進程將其訪問的數據傳回父進程。由於文件或管道是在子進程以比父進程更低的特權運行時打開的,因此不太可能被欺騙做一些不應該做的事情。

以下是一種相對安全地執行反引號的方法。請注意,exec 並不使用 shell 可能展開的字符串來調用。這絕對是調用可能受到 shell 轉義的東西的最佳方法:根本不要調用 shell。

        use English;
        die "Can't fork: $!" unless defined($pid = open(KID, "-|"));
        if ($pid) {           # parent
            while (<KID>) {
                # do something
            }
            close KID;
        } else {
            my @temp     = ($EUID, $EGID);
            my $orig_uid = $UID;
            my $orig_gid = $GID;
            $EUID = $UID;
            $EGID = $GID;
            # Drop privileges
            $UID  = $orig_uid;
            $GID  = $orig_gid;
            # Make sure privs are really gone
            ($EUID, $EGID) = @temp;
            die "Can't drop privileges"
                unless $UID == $EUID  && $GID eq $EGID;
            $ENV{PATH} = "/bin:/usr/bin"; # Minimal PATH.
	    # Consider sanitizing the environment even more.
            exec 'myprog', 'arg1', 'arg2'
                or die "can't exec myprog: $!";
        }

類似的策略也適用於通過 glob 進行通配符展開,雖然您可以使用 readdir 代替。

當您雖然相信自己不會寫出一個洩漏機密的程式,但您不一定相信最終使用它的人不會試圖騙它做一些壞事時,污點檢查是最有用的。這是一種有用於 set-id 程式和代表別人啟動的程式(例如 CGI 程式)的安全檢查。

然而,這與甚至不相信程式碼的作者不會試圖做壞事是完全不同的。當有人把一個你從未見過的程式交給你並說:“這裡,運行這個。”時,你需要這種信任。對於這種安全性,你可能想檢查一下 Perl 發行版中標準包含的 Safe 模塊。該模塊允許程序員設置特殊的區域,其中所有系統操作都被捕獲,並且命名空間訪問受到精心控制。但是,Safe 不應被認為是防彈的:它無法阻止外部代碼設置無限循環、分配大量內存,甚至濫用 perl 的 bug 使主機解譯器崩潰或表現出不可預測的行為。無論如何,如果您真的關心安全性,最好完全避免使用。

Shebang 競賽條件

除了從給予像腳本這樣靈活的系統特權所產生的明顯問題之外,在許多版本的 Unix 上,set-id 腳本從一開始就是不安全的。問題是在內核中存在競賽條件。在內核打開文件以查看要運行的解譯器的文件以及(現在設置-id 的)解譯器重新打開文件以進行解譯之間的時間內,該文件可能已經發生了變化,特別是如果您的系統上有符號鏈接時。

某些 Unix,特別是較新的版本,沒有這個內在的安全漏洞。在這些系統上,當內核將 set-id 腳本的名稱傳遞給解譯器時,它不是使用容易受到干擾的路徑名,而是使用 /dev/fd/3。這是一個特殊的文件已經在腳本上打開,因此惡意腳本無法利用競賽條件。在這些系統上,Perl 應該編譯為 -DSETUID_SCRIPTS_ARE_SECURE_NOW。用於構建 Perl 的 Configure 程序會試圖自行找出這一點,因此您不應該自行指定。大多數現代的 SysVr4 和 BSD 4.4 發行版都使用這種方法來避免內核競賽條件。

如果您沒有安全版本的 set-id 腳本,還不算失敗。有時可以禁用這個內核的“功能”,使內核不運行帶有 set-id 或根本不運行它們的腳本。無論哪種方式都避免了競賽條件的利用,但並不幫助實際運行設置-id 的腳本。

如果核心設定ID腳本功能未被禁用,則任何設定ID腳本都提供了一個可利用的漏洞。Perl無法避免被利用,但會在能夠的情況下指出易受攻擊的腳本。如果Perl檢測到自己被應用於設定ID腳本,則會大聲抱怨您的設定ID腳本不安全,並且不會運行它。當Perl抱怨時,您需要從腳本中刪除設定ID位,以消除漏洞。拒絕運行腳本本身並不會關閉漏洞;這只是Perl鼓勵您這樣做的方式。

要實際運行一個設定ID腳本,如果您沒有安全版本的設定ID腳本,則需要在腳本周圍放置一個C包裝器。C包裝器只是一個編譯後的程序,除了調用您的Perl程序外什麼都不做。編譯後的程序不受困擾設定ID腳本的內核錯誤影響。這是一個簡單的C包裝器:

#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

#define REAL_PATH "/path/to/script"

int main(int argc, char **argv)
{
    execv(REAL_PATH, argv);
    fprintf(stderr, "%s: %s: %s\n",
                    argv[0], REAL_PATH, strerror(errno));
    return 127;
}

將此包裝器編譯成二進制可執行文件,然後使其成為您的腳本的setuid或setgid,而不是您的腳本。請注意,此包裝器未對執行環境進行任何消毒處理,只是確保使用安全的腳本路徑。它只是避免了井號比賽條件。它依賴於Perl自己的特性,以及腳本本身的小心,使其足夠安全以運行腳本的setuid。

保護您的程序

有多種方法可以隱藏您的Perl程序的源代碼,其安全級別各不相同。

首先,您無法取消讀取權限,因為源代碼必須是可讀取的才能進行編譯和解釋。(這並不意味著CGI腳本的源代碼對網絡上的人是可讀的。)因此,您必須將權限保留在社交友好的0755級別,這使得您本地系統上的人只能看到您的源代碼。

有些人錯誤地將此視為安全問題。如果您的程序執行不安全的操作,並且依賴於人們不知道如何利用這些不安全性,那麼它就不安全。某人通常可以確定不安全的事情並且在不查看源代碼的情況下利用它們。將您的錯誤隱藏起來而不是修復它們的做法,即通過混淆來保護安全性,實際上是沒有什麼安全性可言。

您可以嘗試使用通過源代碼過濾器進行加密(來自CPAN的Filter::*,或自Perl 5.8起的Filter::Util::Call和Filter::Simple)。但駭客可能能夠對其進行解密。您可以嘗試使用下面描述的字節碼編譯器和解釋器,但駭客可能能夠對其進行反編譯。您可以嘗試使用下面描述的本機代碼編譯器,但駭客可能能夠對其進行反彙編。這些對於想要獲取您的代碼的人提供了不同程度的困難,但都不能絕對隱藏它(這對每種語言都是如此,不僅僅是Perl)。

如果您擔心人們從您的代碼中獲利,那麼最終的結論是,除了限制性許可證外,沒有任何東西會給您法律安全性。給您的軟件許可證並在其中加入像“這是XYZ公司未發表的專有軟件。您對其的訪問並不意味著您有權使用它等等”的威脅性語句。您應該請律師確保您許可證的措辭在法庭上站得住腳。

Unicode

Unicode是一種新的且複雜的技術,人們可能會輕易忽視某些安全隱患。請參閱perluniintro獲得概述,參

演算法複雜度攻擊

某些 Perl 實作中使用的內部演算法可能會受到攻擊,攻擊者會精心選擇輸入以消耗大量時間、空間或兩者皆有,進而導致所謂的服務拒絕 (DoS) 攻擊。

更多信息請參見 https://www.usenix.org/legacy/events/sec03/tech/full_papers/crosby/crosby.pdf,以及任何有關算法複雜度的計算機科學教科書。

使用Sudo

流行工具sudo提供了一種受控的方式,讓用戶以其他用戶的身份運行程序。它在某種程度上對執行環境進行了清理,並且會避免shebang競賽條件。如果您沒有安全版本的set-id腳本,那麼sudo可能是以另一個用戶身份運行腳本比編寫C包裹更方便的方式。

然而,sudo將真實用戶或組ID設置為目標身份的ID,而不僅僅是有效ID,就像set-id位一樣。結果,Perl無法檢測到它是在sudo下運行,因此不會自動採取自己的安全預防措施,例如啟用taint模式。當sudo配置指定可以運行的確切命令時,批准的命令可能包括一個-T選項,以啟用Perl的taint模式。

一般來說,有必要評估腳本在特定的執行環境中是否適合在sudo下運行。腳本適合在傳統的set-id安排下運行,這既不是必要條件也不是充分條件,儘管許多問題是相互重疊的。

參見

perlrun中的"環境"部分描述了清理環境變量的過程。