当前位置:Linux教程 - Linux文化 - CGI 安全问题

CGI 安全问题


在计算机领域——尤其在Internet上——尽管大部分Web服务器所编的程序都尽可能保护自己的内容不受侵害,但只要CGI脚本中有一点安全方面的失误--口令文件、私有数据、以及任何东西,就能使入侵者能访问计算机。遵循一些简单的规则并保持警惕能使自己的CGI脚本免受侵害,从而可以保护自己的权益。

1. 脚本和程序

在开始决定采用何种语言编写CGI脚本时应考虑几个因素,其中之一应是安全性。Shell 脚本,Perl程序和C可执行程序是CGI脚本最常采用的形式,从安全性角度来说每种都备有优缺。尽管没有哪一种是最好的--基于其他方面的考虑,如速度和可重用性--每种都有实用的领域。

Shell脚本一般用于小的、快速的甚至可以用完就不要的CGI程序,因此,编写它们时常常不考虑安全性。这种疏忽可以导致一些缺陷,使得仅对系统具有一般知识的人也能进入系统任意走动。

尽管Shell CGI 程序最容易写,甚至只需拼凑一下即可,但控制它们却很困难,因为它们一般是通过执行外部的其他程序来完成工作的。这就导致一些可能的隐患,CGI 程序会继承任何它使用的程序的安全问题。

例如,常用UNIX实用程序 awk对于它能处理的数据的数量有一些相当严格限制。如果在CGI脚本中使用awk,那么该程序也就有了同样的限制。Perl比Shell脚本更进一步。Perl用于CGI编程有很多优点,并且相当安全。但Perl能给CGI 作者提供足够的灵活性从而导致对安全性的错误感觉。例如,Perl是解释型的。这意味着它实际在调用时是先编译,然后每次执行一步。这就很容易使得不正确的用户数据被包括进来作为代码的一部分,从而错误地进行解释,形成程序中止原因。

最后谈谈C。C迅速成为标准应用开发语言,几乎所有的UNIX和windows NT系统都是用它开发的。从安全性的角度来看C 似乎是很不错,但由于它的流行性,它的好几种安全性问题已广为人知,而这些问题也能很容易地被人利用。

例如,C 对串处理非常差。它不做任何自动的定位或清理而让编程者自己处理所有事情。在处理串时,大部分C 程序员都是简单地建立一个预定义的空间并希望它足够大以便处理用户输入的任何内容。

当然,Shell脚本、Perl和C 不是仅有的编写CGI脚本语言。实际上,任何可以按预定义的方式与Web服务器进行交互的计算机语言都可以用于编写CGI程序。在UNIX和Windows NT服务器上,数据是通过环境变量和标准输入(stdin) 传给脚本的,所以任何能从这两种数据源读取并写入标准输出(sidout)的语言都能用于创建CGI:awk、FORTRAN、C++、Basic和COBOL,等。windows的程序员可以使用流行的Visual Basic,这意味着有经验的VB程序员不必去学一门新语言。Macintosh使用AppleEvents、和AppleScript与CGI程序进行通信,所以任何可以读写这两者的语言都可使用。

不过,Shell脚本(不管使用那种Shell)、Perl和C仍是最流行为的编写CGI脚本的语言。这并不是说必须使用它们了;只是说大部程序的库——即大部分经过测试的安全的库——都是用这三种语言编写的。如果自己来选择CGI编程语言,最好是借鉴前人的经验。

2. 谁也不信

几乎所有的CGI 安全问题都来自与用户的交互。接收来自外部数据源的输入之后一个简单的、可预见的CGI程序突然向多方向伸展,每个方面都可能有最小的缝隙使得“黑客”可以溜进来。正是与用户的这种交互——通过表单或文件路径——才给予了CGI 脚本这种能力,但同时也使得它们成了运行在Web服务器上的最潜在的危险部分。

编写安全的CGI 脚本很大程度上是创造性和妄想的结合。编写者必须有足够的创造性才能想到用户使用的,不管是无意地还是别的所有的可能隐含导致问题的发送数据的方式。而且必须有点妄想,因为有可能不知道什么时候、什么地方、他们将会一一加以试验。

2.1 两种导致问题的方式

当用户登录进入Web 站点并开始进行交互访问时,他们能以两种方式惹麻烦。一种是不遵守规则,歪曲或违反页面中建立的每个限制或约束;另一种方式是按要求去做。

大部分CGI 脚本是作为HTML表单的后台运行的,负责处理由用户输入的信息并提供某种定制的输出。因为在这种情况下,大部分CGI 脚本编写时都等待某种特殊格式的数据。它们期望用户的输入能匹配收集并发送信息的表单。不过事情并不总是这样。用户可以有许多种办法绕过这些预定义的格式而给脚本发送一些看起来是随机的数据。CGI 程序必须对此有所准备。

其次,用户可以给CGI 脚本发送所期望的数据类型,按预期的形式在表单中填入每个字段。这种类型的提交可以是想像中的来自某个与站点交互的无意的用户,也可能来自某个恶意的“黑客”,凭借他有关操作系统和Web 服务器软件的知识并利用常见的编程错误。这些入侵,表面上一切都正常,却是最危险的、最难检测出来。Web 站点安全性依赖干这种入侵的防止。

2.2 不要相信表单数据

在CGI 编程中最常见的安全失误就是相信从表单传到脚本的数据,用户是未知的一大堆人,他们总能找到一些编程人员从来没想到过的发送数据的方法--而且是程序员认为几乎不可能的方法。

脚本必须对这些加以考虑。例如,下面这些情形都是可能的:

1)从一组单单选按钮中选择的结果可能不是表单中提供的选项之一。

2)来自某个文本字段的数据长度可能大于MAXLENGTH字段允许的长度。

3)字段本身的名字可能与表单中指定的不相符。

2.3 不合理数据的来源

因—些无意的或是有意的原因,导致自己的脚本接收到不知道如何去处理的数据,有可能导致非预期的——同时很危险的——行为。

下面的代码实现了一种表单并向某个搜索yahoo!数据库的CGI脚本送垃圾。该脚本设计得很好并且很安全,因为它忽略了不认识的输入。

<FORM METHOD="POST" ACTION="http://search.yahoo.com/bin/search">
Enter your name,first then last:
<INPUT TYPE="TEXT" NAME="first">
<INPUT TYPE="TEXT" NAME="last">
</FORM

也许用户碰巧(或者意识地)将URL编辑为这个CGI脚本。当浏览器向CGI程序提交数据时,要简单地将输入表单中的数据连到CGI的URL上(用于GET METHODS),就像用户可以很容易地将Web页面地址输入到他的浏览器一样,用户也可以自己修改发送给这个脚本的数据。

例如,当单击表单上的Submit按钮时,Netscape将一个长串字符放入Location字段,该串由CGI的URL后接一串数据组成,大部分看起来像表单中定义的NAMES和VALUES。如果愿意的话,可以自由地编辑Location字段的内容并按自己的意愿修改数据:增加表单中没有的字段,扩展由MAXLENGTH选项限制的文本数据,或者几乎任何对象。以下显示了某CGI脚本预期从表单中提交的URL。

http://www.altavista.digit.com/cgi-bin?pg=q&what=web&imt=&q=%22An+Entirely+Other%22

用户可以修改同一URL,CGI脚本仍被调用,但现在接收的是非预期的数据。为了保证安全,该脚本应该在编写时就设计为能将这种输入识别为不被要求的数据并加以拒绝。

最后,某个有野心的"黑客"也许会写一个程序连到Web上的服务器并假装是一个Web浏览器。该程序可能做一些任何一个真正的web浏览器从未做过的事,例如给CGI脚本发送成百兆字节的数据。如果CGI脚本不限制从POST METHOD读取数据,那怎么办?它有可能会崩溃,也许允许那个崩溃了系统的人访问系统。

2.4 拒绝不合要求的表单数据

CGI脚本可以有几种方式拒绝接收提交给它的非预期的输入。编写CGI时应该使用其中一些技巧或所有这些技巧。

首先,CGI 脚本应设置接收多少数据的限制,不仅限制整个提交,也限制提交中的每个NAME/VALUE对。例如,CGI脚本读取POST METHOD,检查CONTENT-LENGTH环境变量的大小来确定某输入是不是合理的预期输入。如果CGI 脚本设计接收的唯一数据是某人的姓名,那么如果CONTENT-LENGTH大于100字节,就应该有理由返回一个错误。没有哪个合理的姓有那么长,通过设置限制,就能使脚本不再盲目地读取发送给它的内容。

注意

令人高兴的是,不必担心去限制通过POST方法提交的数据。GET是自限制的并且不会向脚本发送多于1KB的数据。服务器自动限制放人QUERY-STRING环境变量中的数据的大小,而这正是GET发送给CGI程序的信息。

当然,"黑客"们可以很容易地将表单由GET改为PUT从而绕过这种内置的限制。至少,程序应该检查一下数据是否是用预期的方法提交的;最好是能正确且安全地处理两种方法。

下一步,应保证脚本知道在接收到不能识别的数据时该怎么办,例如,如果某表单要求用户选择两个单选按钮之一,脚本就不应该假设因为一个按钮未被选择,另一个就一定被选择了。下面的Perl代码就犯了这样的错误:

if ($form_Data{"radio_choice"} eq "button_one"){
# Button One has been clicked }
else {
# Button Two has been clicked }

这段代码假定因为表单仅提供了两个选项,而第一项未被选中,那么第二项就肯定被选中了。这不一定是真的。尽管前面的例子没有什么害处,但在某些情况下这样的假设可能很危险。

CGI脚本应该能预期这种情形而相应地进行处理。例如,如果出现一些非预期的或"不可能"的情形,可以打印一个错误,如下所述:

If ($form_Data{"radio_choice"} eq "button_one") {
#Button One seleted }
elsif ($form_Data{"radio_choice} eq "button_two") {
#Button Two Selected }
else {
#Error }

通过加入第二个if语句--显式检查"radio_choice"实际上是"button_two"--这样脚本更安全了;它不再做假设了。

当然,错误不一定是期望脚本在这些情形下生成的。有些脚本过于小心,验证每个字段,即使是最轻微的非预期数据都生成错误信息,这样往往很扫用户的兴。让CGI 脚本识别非预期数据然后扔掉它,并且自动选择一个缺省值也可以。

另一方面,脚本还可帮助用户纠正错误而不是简单地发一条错误消息或设置一个缺省值。如果表单要求用户输入机密文字,脚本应能在进行比较之前自动跳过输入中的空白字符。下面即是一个完成此功能的Perl程序片段。

$user_input =~ s/\s//;
#Remove white space by replacing it with an empty string
if ($user_input eq $secret_Word) {
#Match! }

最后,可以更进一步,让CGI脚本能处理尽可能多的不同的输入表单。尽管不可能预期到可能发送给CGI程序的所有内容,但对某个特定方面一般经常有几种常用的方式,因而可以逐个检查。

例如,仅仅因为所写的表单使用POST方法向CGI脚本提交数据,并不意味着数据必须按那种方法进来。应该检查REQUEET_METHOD环境变量来确定是使用了GET还是POST方法并相应地读取数据,而不是假定数据都是来自预期的标准输入(stdin)。一个真正编写成功的CGI脚本能接收无论使用什么方法提交的数据并在处理过程中很安全。以下程序清单即是用Perl编写的一个例子。

程序清单 CGI_READ.PL 一个充满活力的读取格式输入的程序

#Takes the maximum length allowed as a parameter
#Returns 1 and the raw form data,or "0" and the error text
sub cgi_Read
{
local($input_Max)=1024 unless $input_Max=$_[0];
local($input_Method)=$ENV{'REOUEST_METHOO');
#Check for each possible REQUEST_METHODS
if ($input_Method eq "GET") {
#"GET"
local($input_Size)=length($ENV{'QUERY_STRING'});
#Check the size of the input
if($input_Size>$input_Max) {
return(0,"input too big"); }
#Read the input from QUERY_STRING
return(1,$ENV{'QUERY_TRING'}); }
elsif ($input_Method eq "POST") {
#"POST"
local($input_Size)=$ENV{'CONTENT_LENGTH'};
local($input_Data);
#Check the size of the input
if ($input_Size>$input_Max) {
return(0,"Input too big"); }
#Read the input from stdin
unless (read(STDIN,$input_Data,$input_Size)) {
return(0,"Could not read STDIN"); }
return(1,$Input_Data);
}
#Unrecognized METHOD
return (0,"METHOD not GET POST");
}

总而言之,脚本应该不对接收的表单数据进行假设,应尽可能预计意料之外的情形并正确地处理不正确的或错误的输入数据。在使用数据之前应按尽可能多的方式测试它;拒绝不合理的输入并打印一条错误消息;如果某项出错或漏了应自动选择一个缺省值;甚至可以试图对输入进行编码以成为程序的合理的输入。选择哪种方式依赖于自己想花费多少时间和精力,不过记住永远也不要盲目接收传给CGI脚来的所有信息。

2.5不要相信路径数据

用户能修改的另一类型数据是PATH_INTO的服务器环境变量。该变量由CGI URL中紧跟在脚本文件名之后的任何路径信息填充的。例如,如果foobar.sh是一个CGl shell脚本,那么当foobar.sh运行时,URL http://www.server.com/cgi-bin/foobar.sh/extra/path/info将导致/extra/path/info被放进PATH_INFO环境变量中。

如果使用这个PATH_INFO环境变量,就必须小心地完全验证它的内容。就像表单数据能以许多种方式被修改一样,PATH_INFO也可以修改。盲目地根据PATH_INFO的中指定的路径文件进行操作的CGI脚本可能会让恶意的用户对服务器造成伤害。

例如,如果某个CGI脚来设计用于简单地打印出PATH_INFO中引用的文件,那么编辑该CGI URL的用户就可以读取机器上的几乎所有文件,如下所示:

#!/bin/sh
#Send the header
echo "Conext-type:text/html"
echo""

#Wrap the file in some HTML
#!/bin/sh
echo"<HTML><HEADER><TITLE>File</TITLE><HEADER><BODY>"
echo"Here is the file you requested:<PRE>\n"
cat $PATH_INFO
echo "</PRE></BODY><HIML>"

尽管在用户只单击预定义的链接(即http://www.server.com/cgi-bin/foobar.sh/public/faq.txt)时,该脚本正常工作,但是一个更有创造性的(或恶意的)用户可能会利用它接收服务器上的任何文件。如果他想进入http://www.server.com/cgi-bin/foobar.sh/etc/passwd,前面的脚本会很高兴地返回机器的口令文件——这可是不希望发生的事。

另一种安全得多的方式是在可能时使用PATH_TRANSLATED环境变量。不是所有的服务器都支持该变量,所以脚本不能依赖于它。不过如果有的话,它能提供完全修饰的路径名,而不是像PATH_INFO提供的相对URL。

不过在某种情形下,如果在CGI脚本中使用PATH_TRANSLATED的话,则可以访问通过浏览器不能访问到的文件。应该知道这点及它的应用。

在大部分UNIX服务器上,htaccess文件可以位于文档树的每个子目录,负责控制谁能够访问该目录中的特殊文件。例如它可以用于限制一组Web页面只给公司雇员看。

虽然服务器知道如何解释.htaccess,从而知道如何限制谁能还是不能看这些页面,CGI脚本却不知道。使用PATH_TRANSLATED访问文件树中任意文件的程序有可能碰巧覆盖了服务器提供的保护。

无论使用PATH_INFO还是PATH_TRANSLATED,另一个重要的步骤是验证路径以确保它或者是一个真正的相对路径或者是脚本认可的几个准确的、预知的路径之一。对于预定的路径,脚本将简单地将提供的数据与认可可以使用的文件的内部清单进行比较,这就是说在增加文件或修改路径时必须重新编译脚本,但安全性却有了保障。只允计用户选择几个预定义的文件而不允许用户指定实际的路径和文件名。

下面是处理访问者提供的路径时应遵循的一些规则。

1)相对路径不以斜线开头。斜线意味着"相对于根"或绝对路径。如果有的话,CGI脚本也是很少需要访问Web根之外的数据。这样它们使用的路径就是相对于Web根目录,而不是绝对路径。应拒绝任何以斜线开始的内容。

2)在路径中单个点(.)和两个点(..)的序列也有特殊含义。单点意味着对"对于当前目录",而双点意味着"相对于当前目录的父目录"。聪明的黑客可以建立象../../../etc/passwd这样的串逆向三层,然后向下进入/etc/passwd文件。应拒绝任何包含双点序列的内容。

3)基于NT服务器使用驱动器字母的概念来引用磁盘卷。包含对驱动器的引用的路径都以一个字母加上一个冒号开头。应拒绝任何以冒号为第二个字符的内容。

4)基于NT的服务器还支持Univesal Naming Conventions(UNC)引用。一个UNC文件规格指定机器名和一个共享点,其余部分与指定机器上的指定的共享点有关。UNC文件规格总是以两个反斜线开头。应拒绝任何UNC路径。

2.6一切看起来都正常,不过…

现在已经知道了用户能给CGI脚本提供非预期的数据的几种方式以及如何对付它们了,余下的更大问题是如何验证用户提交的合法数据。

大部分情况下,正确但聪明地编写的表单提交会导致比越界数据更多的问题。忽略无意义的输入很容易,但确定合法的、正确格式的输入会不会导致问题就要困难得多。因为CGI脚本非常灵活,几乎可做计算机能做的任何事情,所以安全方面的一个很小失误往往能被无限制地加以利用——而这正是最危险的地方。

2.7 处理文件名

文件名是提交给CGI脚本的简单数据,但如果不小心的话,却能导致许多麻烦。如果用户输入的名字中包含路径因素,如目录斜杠和双点,尽管期望的是输入一个简单的文件名--例如file.txt--但结果却可能是/file.txt或../../../file.txt。根据Web服务器的安装以及对提交的文件名做什么操作,系统中的所有文件就有可能都暴露给了一个聪明的黑客。

进一步,如果用户输入了一个已有文件的名字或者一个对系统的运行很重要的文件名,怎么办?对如果输入的名字是/etc/passwd或C:\WINNT\SYSTEM32\KRNL32.DLL怎么办?根据在CGI脚本中对这些文件进行什么操作,它们有可能被发送给用户或者被垃圾覆盖了。在Windows 95和Windows NT下,如果不检查反斜杠字符(\),可能会允许Web 浏览器通过UNC文件名访问甚至不在该Web机器上的文件。

如果用户在文件名中输入了不合法的字符怎么办?在UNIX下,任何以句点(.)开头的文件名都是不可见的。在Windows下斜杠(/)和反斜杠(\)都是目录分隔符。很可能不小心写了一个Perl程序,当文件名以管(pipe)(|)开头时,尽管自己以为仅仅是打开了一个文件,实际上却是执行了一个外部程序。如果用户知道怎么办的话,甚至可以把控制字符(例如Escape键或Return键)作为文件名的一部分送给脚本。

更坏的情况是,在shell脚本中,分号用于结束一条命令并开始另一条命令。如果脚本设计目的是cat用户输入的文件,用户可能输入file.txt;rm-rf/作为文件名,导致返回fi1e.txt,然后清除整个硬盘而不经任何确认。

2.8 输入合理,输出却不合理

为了避免所有这些问题,关闭由它们打开的所有安全缝隙,检查用户输入的每个文件名。必须确保输入正是程序预期的输入。

这样做的最好办法是将输入的文件名的每个字符与可接收字符的清单进行比较,如果不匹配就返回一个错误。这比维持一个所有合法字符的清单并比较它们要安全得多——要想让什么字符溜掉太容易了。

以下程序清单是用Perl如何完成这种比较的例子。它允许任何字符字母(大写或小写调)、任何数字、下划线和句点。它还进行检查以确保文件名不以句点开头。这样,该段代码就不允许可以改变目录的斜杠,不允许可以将多条命令放在一行的分号,或者破坏Perl的Open()调用的Pipes了。

程序清单 保证所有字符都是合法的

if (($file_Name =~ /[^a-zA-Z_\.]/) || ($file_Name =~ /^\./)) {
#File name contains an illegal characgter or starts with a period
}

警告

尽管上述程序清单中的代码清除了大部分不合法的文件名,但操作系可能还有一些限制,而该代码没有覆盖到。例如,文件名可以用数字开头吗?或者以下划线开头?如果文件中包含多个句点或者句点后多于三个字符怎么办?整个文件名足够短得能满足文件系统的限制吗?

必须不断向自己提出这种问题。在写CGI脚本时最危险的事是认为用户会遵守指令。其实用户是不会的。保证用户不犯错误是编程者自己的事。

2.9 处理HTML

另外一种看起来无害的但却能导致很大麻烦的输入是在请求用户输入文本信息时得到的HTML。以下的程序清单是一个Perl程序片段;它向任何在$user_Name变量中输入了一个名字的人,例如John Smith,发出问候信息。

程序清单 发出定制的问候脚本

print ("<HTML><TITLE>Greetings!<TITLE><BODY>\n");
print ("Hello,$user_Name! It's good to see you!\n");
print ("</BODY><HTML>\n");

想像一下,如果用户不是仅仅输入一个名字,而是输入了<HR><H1><P ALIGN="CENTER">John Smith</P><H1><HR>或想像一下当脚本希望得到用户名时,黑客输入了<IMG SRC="/secret/cutekid.gif">,结果是公开了本该保密的信息。允许输入HTML可能很危险。

比输入简单的HTML修改页面或访问画面更危险的是恶意的黑客可能输入一条服务器端的include指令。如果web服务器设置为服从服务器端include,用户就可以输入

<!--#include file="/secret/project/p1an.txt"-->

而不是他的名字,以便看到秘密计划的全部文本,或者用户可以输入<!--#inc1ude fi1e-"/etc/passwd"-->来获取机器的口令文件。可能最坏的情况是黑客可能输入<!--#exec cmd="rm-rf/"-->而不是他的名字。这样上述程序清单中的代码会删掉硬盘上几乎所有内容。

警告

由于经常被恶意地使用,服务器端的include经常被禁止使用以保护站点免受侵害。现在假定这些都没问题。即使关闭了服务器端的include并且不介意用户能看到自己硬盘上的任何图片或者改变页面显示的外观,也仍然有问题--不仅是针对编程者的,而且针对其他用户。

CGI脚本的一个通常用途是留名册(guestbook):访问站点的顾客可能签个名,让别人知道他们已经在那儿了。一般情况下用户简单地输入他的名字,该名字会在访问者清单中出现。但是,如果将The last signee!<FORM><SELECT>作为用户名输入怎么办?<SELECT>标记将导致Web浏览器忽略位于<SELECT>和一个不存在的</SELECT>之间的所有内容,包括以后清单中加入的任何名字。即使有10个人签了名,仅有前3个会显示出来,因为第三个名字包含一个<FORM>和一个<SELECT>标记。因为第三个签名者在他的名字中使用了HTML标记,他后面的任何名字都不会显示出来。

对于用户输入HTML而不是普通的文本的情况有两种解决办法:

1)快速但比较粗糙的办法是不允许小于号(<)和大于号(>),因为所有HTML标记必须包含在这两个字符中,所以清除它们(或者如果碰到它们就返回一个错误)是一种防止HTML被提交并返回的简单的办法。下面一行Perl代码简单地清除了这两个字符:$user_Input=~s/<>//g;

2)更精细一点的办法是将这两个字符转换成它们的HTML换码--—种特殊的代码,用于表示每个字符而不使用该字符本身。下面的代码通过全部用<替换了小于符号,用>替换了大于符号,从而完成了转换:

$user_Input=~s/</&1t;/g;
$user_Input=~s/>/>/g;

2.10 处理外部进程

最后,CGI脚本如何与带有外部过程的用户输入打交道是应该警惕的另一区域。因为执行一个位于自己的CGI脚本之外的程序意味着无法控制它做什么,必须尽最大努力在执行开始前验证发送给它的输入。

例如,shell脚本经常错误地将一个命令行程序和表单输入合在一起执行。如果用户输入符合要求,一切都挺正常,但是有可能会加入其它命令并非法执行。

下面即是一个产生了这种错误的脚本的例子:

FINGER_OUTPUT='finger$USER_INPUT'
echo $FINGER_OUTPUT

如果用户很礼貌地给finger输入了某人的e-mail地址,一切都会正常工作,但是如果他输入了一个e-mail地址,后面再跟一个分号和另一条命令,那么该命令也会被执行,如果用户输入了[email protected];rm-rf/,那麻烦可就大了。

即使没有什么隐藏的命令被加入用户数据,无意的输入错误也可能带来麻烦。例如,下面的代码行会产生一个意料之外的结果——列出目录中的所有文件——如果用户输入是一个星号的话。

echo "Your input:"$USER_INPUT

当通过shell发送用户数据时,就象前面的代码片段所做的那样,最好检查一下shell的meta-character(元字符)——这些可能会导致意外的行为。

这些字符包括分号(允许一行中有多条命令),星号和问号(完成文件匹配),感叹号(在csh下指运行的作业),单引号(执行一条包含其中的命令)等等。就像过滤文件名一样,维护一个允许的字符清单一般要比试图找出每个不允许的字符容易一些。下面的Perl代码片段验证一个e-mail地址:

if ($email_Address ~= /[^a-zA-z0-9_\-\+\@\.]) {
#lllegal character! }
else { system("finger $email_Address"); }

如果决定在输入中允许shell元字符,也有办法让它们安全一些。尽管可以简单地给未验证的用户输入加上引号以免shell按特殊字符进行操作,但这实际上不起什么作用。请看下的语句:

echo"Finger information:<HR><PRE>"
finger"$USER_INPUT
echo"</PRE>

尽管$USER_INPUT上的引号可以使shell不再解释一个分号,从而不允许黑客简单地插进来一条命令,但该脚本仍有许多安全方面的漏洞。例如,输入可能是'rm-rf/',其中单引号可以导致甚至在finger不知道的情况下执行黑客的命令。

一种处理特殊字符的较好的办法是对它们进行换码,这样脚本只是取它们的值而不解释它们。通过对用户输入进行换码,所有的shell元字符都被忽略并作为增加的数据传给程序。下面的Perl代码即对非字母数字字符完成这种处理。

$user_Input=~s/([^w])/\\\1/g;

现在,如果用户输入加在某条命令之后,每个字符——即便是特殊字符——都会由shell传送给finger。

不过请记住,验证用户输入——不相信发送给自己的任何信息——会使自己的代码更易读并且执行起来更安全。最好不是在已经执行了命令之后再去对付黑客,而应在门口就对数据进行一次性的检查。

--------------------------------------------

处理内部函数

对于解释型语言,例如Shell和Perl,如果用户输入的数据不正确的话,有可能导致程序生成本来没有的错误。如果用户数据被解释为一部分执行代码,用户输入的任何内容都必须符合语言的规则,否则就会出错。

例如,下面的Perl代码片段也许会正常工作也许会产生错误,这取决于用户输入的是什么:

if ($search_Text =~ /$user_Pattern/) {
#Match! }

如果$user_Pattern是一个正确的表达式,一切都会正常,但是如果$user_Pattern不合法;Perl就会失败,导致CGI程序失败——这可能是一种不安全的方式。为了避免这种情况,在Perl中至少应有eval()操作符,它计算表达式的值并与执行它无关,返回一个码值表示表达式是有效的还是无效的。下面的代码即是前面代码的改进版。
if (eval{$search_Text =~ /$user_Pattern/}) {
if ($search_Text =~ /$user_Pattern/) {
#Match!
}
}

不幸的是,大部分shells(包括最常用的,/bin/sh)都没有像这样的简单的办法检查错误,这也是避免它们的另一原因。

--------------------------------------------

在执行外部程序时,还必须知道传送给那些程序的用户输入是如何影响程序的。编程者可以保护自己CGI脚本不受黑客侵犯,但是如果轻率地将某个黑客输入的内容传送给了外部程序而不知道那些程序是如何使用这些数据的,也会徒劳无益。

例如,许多CGI脚本会执行mail程序给某人发送一个包含用户输入信息的e-mail。这可能会非常危险,因为mail有许多内部命令,任何一个命令都有可能被用户输入激活。例如,如果用mail发送用户输入的文本而该文本有一行以代字号(~)开头,mail会将该行的下一字符解释为它能执行的许多命令之一。例如,~r/etc/passwd,会导致mail读取机器的口令文件并发送给收信人(也许正是黑客自己)。

在这样的例子中,应该使用sendmail(一个更底层的邮寄程序,它少了许多mail的特性),而不是使用mail在UNIX机器上发送e-mail。

作为一般规则,在执行外部程序时应该使用尽可能贴近自己要求的程序,不必有过多不必要的功能。外部程序能干的事越少,它被利用来干坏事的机会就越少。

警告

下面是使用mail和sendmail的另一个问题:必须保证发送给mail系统的是一个合法的e-mail地址。许多mail系统都会把以"|"开头的e-mail地址作为要执行的命令,从而为输入这样一个地址的黑客打开方便之门,请再一次记住要验证数据。

怎样才能更好地了解外部程序以便有效地使用它们的另一个例子是grep。grep是一个简单的命令行实用程序,它在文件中搜索一个常用表达式,表达式可以是一个简单的串也可以是复杂的字符序列。大部分人会说使用grep不会出什么问题,但是尽管grep可能不会造成什么损失,它却能被愚弄,下面将说明它是怎么被愚弄的,如下面的代码所示。它假定在许多文件中完成对用户输入项的区分大小写的搜索。

print("The following lines contain your term:<HR><PRE>");
$search_Term=~s/([^w])/\\\1/g;
system("grep $search_Term/public/files/*.txt");
print(<"PRE>");

这一切看起来挺好,除非考虑到用户可能会输入-i。它不会被搜索,而是作为与grep的切换,就像任何以连字符开头的输入一样。这会导致grep或者因等待将搜索的串输入标准输入而挂起,或者如果-i后的内容被解释为其他切换字符时产生错误。毫无疑问这不是编程者本来的意图。在这种情况下它还不太危险,但在其他情况下却有可能。记住,没有什么无害的命令,对每条命令部必须从各个角度仔细考虑。

一般情况下,应该尽可能熟悉自己的CGI脚本执行的每个外部程序。对程序知道得越多,就越能保护它们免受数据破坏--一方面可以监视数据,另一方面可以禁止某些选项或特性。外部程序经常是许多CGI程序问题的一种快速方便的解决办法——它们都经过了测试,可以得到,并且灵活多样。但它们也可以成为黑客入侵的方便之门。不要害怕使用外部程序——它们经常是完成CGI程序中某种功能的唯一办法——但是要知道它们可能带来的危害。

3 内部伤害

到目前为止,仅仅考虑了通过Web例览站点的人——从几千里之外——可能带来的潜在的安全危险。但实际上还存在另一种离得更近的危险因素。

在CGI安全问题上常犯的一种错误是忘记了本地用户。尽管通过Web浏览站点的人不影响本地安全,如文件保护和所有者,但Web服务器的本地用户却能这样,必须做出更多努力防止这些入侵。大部分多用户系统上,如UNIX,Web服务器是作为一个程序运行的,而机器仍被许多人使用做着许多事情。仅仅因为为某人与自己一起工作或访问自己的学校并不意味着他能抵制住诱惑,而不去捣鼓Web安装从而引起问题。

3.1 CGI脚本用户

大部分Web服务器是作为运行CGI脚本的特殊用户而安装的。这是在CGI程序运行时拥有该CGI程序的用户,并且他所拥有的权限能限制该脚本能做什么事情。

在UNIX下,服务器自己也是作为root(系统的超级用户或管理员)运行的,并允许它使用端口80作为浏览器与之通信的地方(只有root能使用这些被称为"保留的"端口0到1023;所有用户都可以使用其余的端口)。当服务器执行CGI程序时,大部分Web服务器都能设置为以另外一个用户而不是Web服务器本身来运行该程序——尽管不是所有服务器都能这么做。

将CGI脚本作为root运行是很危险的!服务器应被设为利用一个普通用户,如常用的nobody来运行CGI脚本。用户权限越小,运行的CGI脚本能造成的危害就越小。

3.2 Setuid 危险

编程者还应知道自己的UNIX CGI脚本中是否设置Setuid位。如果对某个可执行文件允许该选项,将能使该程序与拥有该文件的用户有同样权限,而不是执行它的用户。如果自己的CGI脚本上设置setuid位,无论服务器作为什么用户来运行它,它的权限都等同于该文件的拥有者。这当然有很大的隐患--可能会对以其权限运行脚本的用户失去控制。幸运的是Setuid位很容易被禁止。对所有CGI脚本执行chmod a-s即能关闭所有的setuid,程序即能以允许的权限运行。

当然,在某些情况下也许希望设置setuid位--例如如果脚本需要以特殊用户身份来运行以访问一个数据库。在这种情况下,必须加倍小心确保该程序的其他文件保护能将可以访问它的用户限制在允许范围内。

3.3 "Community" Web服务器

即使Web服务器以一个常用的用户来执行脚本,仍有一个潜在的问题,那就是一个人并不总是能控制服务器。如果许多人共同控制服务器,每个人都可以将CGI脚本安装作为nobody用户来运行。这就使这些人的任何一个都可以利用CGI程序访问他们原先不能访问的地方,而这些地方是nobody允许进入的。

也许潜在的安全问题的解决办法是将CGI的控制限制为一个人。在某些情况下尽管这似乎是合理的,但对较大站点却经常不太可能。例如,一个大学有几百个学生,每个学生都想试着去编写并安装CGI脚本。

3.4 使用CGI Wrap

当有多个用户可以访问CGI时,对于确定脚本以什么用户运行的问题的一个较好的解决办法是CGI wrap程序。CGI Wrap,可以在using CGI Web站点中找到,是一个简单的包装,它以拥有该文件的用户而不是服务器指定的用户来运行CGI脚本。这种简单的预防措施使脚本拥有者对它可能的危害负责。

因为CGI wrap使得CGI脚本的作者负责他们自己的脚本权限,所以它不仅是一个保护其他人拥有的重要文件的有力的工具,而且是促使人们编写安全的脚本的有力的工具。只有他们自己的文件会处于危险之中,这样的现实对脚本作者会是极大的促进。

3.5 CGI脚本权限

还应该清楚了解CGI脚本被哪个用户拥有以及脚本自身的文件权限。包含脚本的目录的权限也非常重要。

例如,如果Web服务器上的cgi-bin目录是所有人可写的,那任何本地用户将能删除CGI脚本并用另一个来代替。如果脚本本身是所有人可写的话,那么任何人将能修改脚本完成任何事情。

请看下面这段无害的UNIX CGI脚本:

#!/bin/sh
#Send the header
echo"Content-type:tex/html"
echo""
#Send some HTML
echo "<HTML><HEADER><TITLE>Fortune</1TLE><HEADER>
echo "<Body>Your fortune:<HR><PRE>
forune
echo"</BODY><HIML>"

现在,如果脚本上设置的权限允许某个恶意的用户将程序改变如下:

#!/bin/sh
#Send the header
echo "content-type:text/html"
echo""
#Do some damage!
rm-rf/
echo"<HTML><TITLE>Got you! <TITLE><BODY>"
echO"<H1>Ha ha!<H1></BODY></HTML>"

那么下一个在Web上访问该脚本的用户即使他没做什么坏事也会导致大量问题。在Web上检查用户输入的完整性很重要,但更重要的是保证脚本本身未被修改且不能被修改。

3.6 本地文件安全

脚本在本地硬盘上创建的文件的完整性也同样重要。在得到Web用户输入的一个合理的文件名之后,使用该文件名干什么也很重要。根据Web服务器运行的操作系统,权限和拥有者信息可以与文件中的数据一起存在文件上。

例如,UNIX系统能记录文件访问权限,包括创建该文件的用户的权限、同组用户的权限、以及系统其他人的权限。windows NT使用的是一个更复杂访问控制清单系统,但完成的功能大致相同。根据这些标志如设置以及授予或禁止什么权限,Web服务器机器的用户也可能引起麻烦。

例如,在创建一个文件时就应知道给它设置的权限。大部分Web服务器软件将umask或权限码设为0000,意味着可以创建一个任何人可读写的文件。尽管文件上的权限设置对在Web上浏览的人可能没什么不同,但本地访问的用户却能利用不严格的权限设置造成危害。基于这种现实,应该尽可能严格地限制文件权限。

保证每个打开文件的调用都有一个最小限制集合的最简单的办法是设置脚本的umask。umask()是一个UNIX调用,它能对每个后续的文件创建限制权限。umask()的参数是一个数字,用于对后续的文件创建的权限码进行屏蔽。如果umask为0022,则不管在打开文件时给组用户和其他用户赋予了什么显式的权限,都将导致创建的文件仅能被用户自己写。即使已经设置了umask,创建文件时也应该显式指定权限。如果只有CGI脚本能访问文件,那么只有运行CGI程序的用户才能访问该文件——权限为0600。如果另一个程序需要访问该文件,可以使该程序的拥有者成为与CGI脚本同一组的用户,这样只需设置组用户权限——权限为0660。如果必须让所有人都能访问该文件,应使该文件只能读,不能写——权限为0644。

3.7 使用显式路径

最后,本地用户还可以最后一种方式攻击Web服务器——欺骗服务器运行他写的一个外部程序,而不是运行在CGI脚本中指定的程序。下面是一个简单的程序,从UNIX的fortune命令可以看出该浏览者还比较聪明。

#!/bin/sh
# Send the header
echo"conten_type:text/html"
echo""
#Send the fortune
echo"<HTML><HEADER><TITLE>Fortune</TITLE></HEADER><BODY>"
echo "<You crack open the cookie and the fortune reads:<HR><PRE>"
fortune
echo "</PRE><BODY></HTML>"

该脚本看起来可一点没有害处。它不接收用户输入,所以用户不能籍此搞什么把戏。因为它仅由Web服务器运行,所以脚本本身的权限设置可以非常严格,可以防止任何有企图的本地用户修改它。如果对该脚本所在的目录也设置了正确的权限的话,看起来就没什么地方可以出问题了,是不是?

当然还有问题。记住得要有点偏激。

上述程序清单调用了外部程序,在本例中是echo和fortune。因为这些脚本没有用显式路径指明它们在硬盘上的位置,该shell即使用PATH环境变量来找到它们,从变量中的每一项查找要执行的程序。这可能很危险。例如,如果fortune程序安装在/usr/games中,但PATH中在它之前列出了/TMP,那么任何碰巧命名为"fortune"并位于临时目录的程序都会被执行,而不是真正的fortune。

该程序可以做它的创建者想做的任何事情,可以删除文件,也可以登记有关请求信息并将数据传给真正的fortune——使用户和编程者谁也不聪明。在CGI脚本中运行外部程序时一定要指定显式的路径。PATH环境变量有很大作用,但它与其他变量一样也能被非法使用。

4 使用他人CGI脚本时的注意事项

关于CGI,可以从很多地方获得信息——从Internet上,从学校图书馆中,从像本书这样的书中,UseNet组中以及朋友和同事中。从这些地方不仅可以获得信息,还可以得到实际的程序和库。有些程序和库如果已经有人做过了为什么自己还要从头再做一遍呢?但就像不能盲目听从别人的意见一样,关于如何理财,如何驾车或者生活中的别的方面,同样,也不能在自己的服务器上盲目地运行另从的代码。从Net上得到的脚本也可能真正是很好的脚本。但也许并不是。花些时间考察一下脚本的来源以及获取它的站点的可靠性是值得的。

4.1 追根求源

某些Web拥有者。如果不能看到并研究源代码的话,他们甚至都不会运行一个公共的、免费的或商业性的脚本。这可能有点偏激。如果某个声誉很好的公司销售一个文档详细且广为使用的脚本,该脚本应该比自己写的脚本更安全一些。原因有二。首先,专业人才知道并能避免一些常见的安全漏洞;其次,公司是为了嫌钱而做生意,如果他们以次充好或销售那些恶意的产品就不能再做生意赚钱了。

从另一方面来看,如果UseNet组中看到一个编译好的可执行文件出自一个从没听说过的人,没有什么文档可以看,也没有该程序的用户可以交流交流,那么在将它放入自己的服务器之前一定要仔细考虑。也有可能这是来自一个像自己一样的另一个CGI编程者的完全合法的贡献,目的是想让全世界共享他的编程成果。但它也可能来自某个恶意的,具有变态幽默感的,只想看到自己能使多少人清盘的人。

在评价公共的免费软件或商业性软件时,应考虑下面这些方面:

该脚本来自一个声誉好的站点吗?该站点存在很长一段时间了吗?它维护得好吗?Web拥有者在发布文件前进行检查吗?

有没有足够的文档说明该程序如何工作以及用户如何使用等信息?

有多少人已经下载了该脚本?该站点愿意提供顾客名单吗?(仅在有疑问时才去询问;Web拥有者不会整天去回答这类问题。)

有人在UseNet上讨论该脚本吗?如果有,他们说好还是不好?如果没人提到该脚本可以进一步请求别人的见解。一般总会有人响应的。

提示

在评价脚本时检查下面这些useNet组: comp.security.announce,comp.securiy.unix,以及comp.infosystem.www.authority.cgi。另外还可以访问位于ftp.cert.org的Computer

Emergency Response Team,以了解安全问题的历史及有关工作以及安全保护的软件。

5)该脚本的作者有没有一些别的好名声的脚本?

6)源代码能得到吗?免费的或有价的都行。

7)该程序是不是过份宣传它的能力?如果是,这可能是一个编程新手。

8)该站点自己运行了该脚本吗?如果没有,为什么?能找到别的站点运行该脚本吗?过分偏激以及时间限制

尽管游览取自Web的所有代码是个好主意,但要花费很多时间,特别是当代码比较复杂时更是如此。

例如,NCSA HTTPd就太大了,一般用户不可能一行行去读,但是从它的主站点http://www.ncsa.uiuc.edu下载它却能保证极好的完整性,满足任何用户的需要。实际上,任何从NCSA下载的东西都是有保障的。

实际上,Web上的许多著名的站点已经为用户做了大部分的几乎偏激的代码检查工作。从它闪中下载代码是可能利用的另一层另一层保护。这些站点包括:

ftp://ftp.ncsa.uiuc.edu/Web/httpd/Unix/ncsa_httpd/cgi(the NCSA Archive)
http://www.novia.net/~geewhiz(Virtual Webwerx Division Zero-CGI Land)
http://www.lpage.com/cgi(the World-Famous Guestbook Server)
http://sweetbay.will.uiuc.edu/cgi++(cgi++)
http://www.aee.com/wdw(the Web Developers Warehouse)

4.2 注意礼貌

最后,如果确实希望从Web上下载一些CGI代码,或者完整地使用它,或者用作自己编写的更大程序的一部分,还应了解一些事情。

代码是兔费的并不意味着可以自由地用它作自己想做的任何事情。通常程序和库是禁止拷贝的,如果原始作者没有放弃这个权力,他即能限制如何使用该程序。例如,作者可能禁止拆散该脚本,及禁止用作别的脚本的一部分。

一跟来说,在使用别人的代码之前(即使已经确定它是安全的),最好与作者进行联系取得许可。至少这样做比较有礼貌。而大部分情况下,作者会很高兴他的代码能被别人利用。当然,如果在自己程序某个片段处注明原始作者将是很礼貌的。

原作者:Jeffry Dwight, Michael Erwin, Robert Nile
本文转载自《CGI 指南》