PERL 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脚本送垃圾。该脚本设计得很好并且很安全,因为它忽略了不认识的输入。


Enter your name,first then last:


$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"
"
echo"Here is the file you requested:

\n"
cat $PATH_INFO
echo "

" 尽管在用户只单击预定义的链接(即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 ("\n"); 想像一下,如果用户不是仅仅输入一个名字,而是输入了


John Smith


或想像一下当脚本希望得到用户名时,黑客输入了,结果是公开了本该保密的信息。允许输入HTML可能很危险。 比输入简单的HTML修改页面或访问画面更危险的是恶意的黑客可能输入一条服务器端的include指令。如果web服务器设置为服从服务器端include,用户就可以输入 而不是他的名字,以便看到秘密计划的全部文本,或者用户可以输入来获取机器的口令文件。可能最坏的情况是黑客可能输入而不是他的名字。这样上述程序清单中的代码会删掉硬盘上几乎所有内容。 警告 由于经常被恶意地使用,服务器端的include经常被禁止使用以保护站点免受侵害。现在假定这些都没问题。即使关闭了服务器端的include并且不介意用户能看到自己硬盘上的任何图片或者改变页面显示的外观,也仍然有问题--不仅是针对编程者的,而且针对其他用户。 CGI脚本的一个通常用途是留名册(guestbook):访问站点的顾客可能签个名,让别人知道他们已经在那儿了。一般情况下用户简单地输入他的名字,该名字会在访问者清单中出现。但是,如果将The last signee!