CGI教学:CGI安全问题(再续)
发布时间:2008-08-06 20:24:26 阅读次数:
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;
.10 处理外部进程
最后,CGI脚本如何与带有外部过程的用户输入打交道是应该警惕的另一区域。因为执行一个位于自己的CGI脚本之外的程序意味着无法控制它做什么,必须尽最大努力在执行开始前验证发送给它的输入。
例如,shell脚本经常错误地将一个命令行程序和表单输入合在一起执行。如果用户输入符合要求,一切都挺正常,但是有可能会加入其它命令并非法执行。
下面即是一个产生了这种错误的脚本的例子:
FINGER_OUTPUT='finger$USER_INPUT'
echo $FINGER_OUTPUT
如果用户很礼貌地给finger输入了某人的e-mail地址,一切都会正常工作,但是如果他输入了一个e-mail地址,后面再跟一个分号和另一条命令,那么该命令也会被执行,如果用户输入了webmaster@www.server.com;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程序中某种功能的唯一办法——但是要知道它们可能带来的危害。