我们现在看到的许多交互web页后面的支持语言就是Perl。只要你往表单中输入数据或从交互报告中获取信息,你大概就是在与Perl打交道。
交互web页使用了一种称做CGI(Common Gateway Interface,公共网关接口)的API来处理用户与应用程序间的交互。因为Perl具有优秀的字符串处理功能,所以,它是编写web应用程序的理想语言。
本章讲解CGI是如何工作的,以及如何编写简单的CGI程序来增强你的网站。
本章面向UNIX或Linux系统,因为这是大部分服务器运行的平台。
12.1 简单CGI
你需要理解CGI程序执行时发生了什么。当用户点击链接或在浏览器中输入链接地址请求一个URL时,CGI就启动了。
浏览器将请求发送到web服务器,告诉它“我希望得到URL名字为http://webserver.com/cgi-bin/hello.pl的资源。”
web服务器查看URL,将它分解为各个组件。关键部分是目录cgi-bin以及文件名hello.pl。通常服务器只返回文件hello.pl,但因为它位于目录cgi-bin中,而且因为服务器的配置告诉它cgi-bin包含CGI程序,所以它执行这一程序,捕获输出并发送到web服务器。
用户的web浏览器于是看到来自web服务器的资源,并显示出来,或让用户保存在硬盘上。到目前为止,只涉及到web浏览器发送请求并反馈到web页。
通过上面的介绍已经对CGI的作用有了一个概念,下面学习如何编写第一个CGI程序。
12.2 CGI版本的“Hello World”
下面编写CGI版本的“Hello World”程序。虽然简单,但这个程序可能比运行一个普通的Perl程序更具挑战性。
程序本身相当简单,如程序清单12-1所示。
程序清单12-1 hello.pl
#!/home/sdo/local/bin/perl –T
# 用你的Perl程序路径替换以上行
use strict;
use warnings;
print <<EOF
Content: text/html
<HEAD><TITLE>Hello</TITLE></HEAD>
<BODY>
<P>
Hello World!
</BODY>
EOF
要注意的第一件事是程序的开始行:
#!/home/sdo/local/bin/perl –T
这是UNIX/Linux系统上使用的一个特殊的“变幻行”,它将脚本转换成一个程序。(参见附录B“将Perl脚本转换成命令”了解详细信息)你必须对这一行进行一些替换,让它包含你的Perl安装完整路径,所以,这一段脚本需要一些改动。
给Perl解释器传递-T开关来打开名叫“taint”的检查。这是一种安全特征,将在后面详细讨论。
然后程序打印如下内容:
Content: text/html
blank line
也就是打印Content: text/html,后面是一个空行。这两行告诉浏览器后面是一个web页。
接着打印web页本身。
12.2.1 运行程序
下一步就是运行这个程序。首先,使用如下命令设置执行权限:
chmod a+rx hello.pl
然后必须将程序放在web服务器可以执行的地方。准确的目录与配置有关。如果你运行的是Apache web服务器,缺省的CGI脚本位置是目录www/cgi-bin以及它的子目录。(此目录随系统的不同而不同。甚至不同的Linux版本将CGI目录放在不同的位置。一些常规的安装目录是~www/var/cgi-bin以及~apache/cgi-bin。)
例如,假如你运行的是Linux,配备Apache,配置为缺省。将脚本移到下面的目录中:
mv hello.pl www/cgi–bin
下一步就是检查是否可以从浏览器运行这个脚本。通过指定脚本的URL来完成。同样,这与系统和配置有关。如果按缺省配置运行Apache(如本例所示),对应的URL就是:
http://www.webserver.com/cgi–bin/hello.pl
将其中的webserver用你的系统名代替。
图12-1是将URL装载到Inernet Explorer窗口中的结果。
图12-1 Hello World(CGI版本)
常见问题:错误目录或配置错误的服务器?
有时,当你输入URL时,出现的是脚本而不是脚本的输出(如图12-2所示)。
问题大概出在将脚本放在错误的目录里了。脚本只有放在允许CGI的目录里时才会执行。如果脚本放在一个普通web页目录里,它就被当成一个普通web页看待和显示。
解决的办法是将脚本放在正确的目录中。可以求助于网站管理人员。
另一方面,如果你确定你放置的目录是正确的,那么问题是可能是web服务器配置错误。
不管是哪一种原因,都可以找web服务器管理员解决问题,也可以阅读本章后面的“调试CGI脚本”一节内容。
图12-2 错误:脚本被显示,而没有被执行
12.3 基本表单
下面的例子创建一个web表单,它允许用户填写一份调查表,并通过网络发送。
图12-3显示了完整的服务器页面。
图12-3 调查表单
调查网页实际上是一个CGI表单。这个例子假定你已经熟悉基本的HTML,但可能不熟悉CGI表单。
网页开始是一些普通的项:
<HEAD><TITLE>Survey</TITLE></HEAD>
<BODY>
<CENTER><H1>Please fill in our survey</H1></CENTER>
接下来就是特殊的表单相关标签了。
12.3.1 FORM语句
假如你负责记帐,你需要指定从服务器获取哪些信息。使用下面的表单来完成。
表单的开始行为:
<FORM ACTION=”http://www.webserver.com/cgi–bin/record.pl” METHOD=”POST”>
这一行在ACTION域使用了一个绝对web地址(开头有http)。也可以使用相对地址:
<FORM ACTION=”/cgi–bin/record.pl” METHOD=”POST”>
相对URL与系统有关。使用相对地址意味着你可以在系统间移动表单,而URL保持指向本地系统。这同时意味着你需要保存有这个CGI脚本。
FORM标签表示这是表单的开始。ACTION按钮告诉浏览器提交表单时所用的URL。
METHOD告诉浏览器如何解码表单中的数据。“GET”和“POST”是两种可能的方法。在“GET”方法中,表单的信息作为URL的一部分而放在一起。例如,如果你看到一个URL如下所示:
http://www.webserver.com/cgi–bin/record.pl?name=sam
你已经明白了使用GET方法所发生的一切。
另一个方法POST将表单数据发送到HTTP头,HTTP头被发送到浏览器来运行程序并得到下一个网页。
GET方法的优点是,因为表单的数据包含于URL中,你可以将提交的URL做成书签。这对于web搜索或股票报价是有用的,因为你可能需要将提交表单后的页面做成书签。
例如,页面http://quote.yahoo.com/q?s=rhat&d=v1包含Yahoo的某个CGI程序生成的当前Redhat股票报价。
不过,如果你不希望信息暴露在屏幕的顶端,就使用POST。GET方法发送的数据长度还限制在1024个字符之内。因此,对于大的表单,必须使用POST。
因为GET方法发送的数据是URL的一部分,所以它对用户是可见的。(它也记录在web服务器的日志中)让数据可见对调试有用,但对于安全性数据并不希望如此,例如密码或信用卡号。
同时因为GET实际是一种增强的URL,所以,你可以将GET表单做成书签。但对于POST方法却不能这样做,因为当你离开页面后,数据也随之消失了。
12.3.2 文本空白
你需要告诉浏览器你需要从用户获取什么数据。第一个数据是用户的名字。对应的代码为:
<P>
Name:
<INPUT TYPE=”text” NAME=”name” SIZE=”30”>
<INPUT>指令告诉浏览器询问用户的信息。TYPE属性告诉浏览器需要什么类型的信息。在这里,它是单行文本(TYPE= “text”)。
NAME属性用于给空白起名,SIZE指定空白有多大。
12.3.3 选项列表
你希望知道用户所属的组别。他可能属于软件组、可能属于市场组或者属于支持组。为了得到这些信息,在web页上放置一个选项列表:
<SELECT NAME=”group” SIZE=1>
<OPTION>Software</OPTION>
<OPTION>Marketing</OPTION>
<OPTION>Support</OPTION>
</SELECT>
<SELECT>标签中放置选项列表。列表中的每个选项都要放于<OPTION>标签中。
12.3.4 复选框
表单中的下面两个项是几个复选框。此时,你需要返回到<INPUT>标签来指定它们:
<INPUT TYPE=”checkbox” NAME=”work” VALUE=”1” CHECKED>
Full time employee</INPUT>
<BR>
<INPUT TYPE=”checkbox” NAME=”intern” VALUE=”1”>
Currently a member of an intern program</INPUT>
第一个复选框叫“work”,如果它被设置则返回值1。CHECKED属性告诉浏览器这一个复选框最初被复选。
第二个复选框没有这样的属性,所以按缺省即没有复选。
12.3.5 单选按钮
两个复选框的下面是三个单选按钮。
对应的代码是:
<P>
I believe that my current project will be completed:
<BR>
<INPUT TYPE=”radio” NAME=”schedule” VALUE=”early”>Early</INPUT>
<INPUT TYPE=”radio” NAME=”schedule” VALUE=”ontime”>On Time</INPUT>
<INPUT TYPE=”radio” NAME=”schedule” VALUE=”late”>Late</INPUT>
TYPE属性是“radio”,即创建单选按钮。这一系列中的所有单选按钮都有同一个名字。在这里就是“schedule”。
系统通过VALUE告诉CGI程序哪个按钮被选中。
因为没有任何单选按钮的属性是CHECKED,所以缺省时没有选中任何按钮。
12.3.6 文本区域
表单的开始是“Name”空白。在这一空白中输入一行信息。现在你需要让用户在此输入注释。这就是一个多行空白。
对应的代码就是:
<TEXTAREA NAME=”comments” COLS=”35” ROWS=”4”>
Replace this with your comments.
</TEXTAREA>
<TEXTAREA>指令起始一个文本框。NAME属性告诉系统空白使用什么标识符。属性CLOS及ROWS指定文本框的宽度和高度。
文本框的初始文本放在<TEXTAREA>和</TEXTAREA>指令之间。
12.3.7 隐藏输入
现在处理不在表单上出现的信息部分。这就是隐藏输入。通过这类INPUT指令可以将信息嵌入表单中,而用户看不到。本例在表单中嵌入了一个版本号。
对应的指令是:
<INPUT TYPE=”hidden” NAME=”version” VALUE=”1.0”>
12.3.8 提交按钮
最后,需要添加一个按钮让用户给程序发送表单中的数据:
<INPUT TYPE=”submit” VALUE=”Submit Form”>
当用户点击这个按钮时,表单就被提交到web服务器。
12.3.9 组合在一起
程序清单12-2是这个web表单的完整版本。
程序清单12-2 survey.html
<HEAD><TITLE>Survey</TITLE></HEAD>
<BODY>
<CENTER><H1>Please fill in our survey</H1></CENTER>
<!–– Replace the ACTION below with the path to your script ––!>
<FORM ACTION=”/cgi–bin/record.pl” METHOD=”POST”>
<P>
Name:
<INPUT TYPE=”text” NAME=”name” SIZE=”30”>
<BR>
Group:
<SELECT NAME=”group” SIZE=1>
<OPTION>Software</OPTION>
<OPTION>Marketing</OPTION>
<OPTION>Support</OPTION>
</SELECT>
<BR>
<INPUT TYPE=”checkbox” NAME=”work” VALUE=”1” CHECKED>
Full time employee</INPUT>
<BR>
<INPUT TYPE=”checkbox” NAME=”intern” VALUE=”1”>
Currently a member of an intern program</INPUT>
<BR>
<P>
I believe that my current project will be completed:
<BR>
<INPUT TYPE=”radio” NAME=”schedule” VALUE=”early”>Early</INPUT>
<INPUT TYPE=”radio” NAME=”schedule” VALUE=”ontime”>On Time</INPUT>
<INPUT TYPE=”radio” NAME=”schedule” VALUE=”late”>Late</INPUT>
<P>
Comments:
<BR>
<TEXTAREA NAME=”comments” COLS=”35” ROWS=”4”>
Replace this with your comments.
</TEXTAREA>
<INPUT TYPE=”hidden” NAME=”version” VALUE=”1.0”>
<BR>
<INPUT TYPE=”submit” VALUE=”Submit Form”>
</FORM>
</BODY>
</HTML>
12.4 创建CGI程序
现在已经创建了web表单,还需要有一个程序来处理它。这一节描述如何让一个简单CGI程序读取表单数据。
当用户点击表单上的Submit(提交)按钮时,浏览器编码表单中的数据并发送到web服务器。web服务器然后运行CGI程序并送回数据。
对表单中的数据进行编码和解码包含很复杂的过程。幸运的是,你不必担心这一点,因为Perl的模块会替你完成这部分工作。
CGI与CGI::Thin
大部分关于CGI的编程工具都描述了如何使用CGI模块,而没有描述CGI::Thin模块。CGI::Thin模块是一个短小、简单的模块,它可以完成你希望做的任何事。
CGI模块也完成你希望做的任何事。问题是CGI模块还包含上百个你不希望的函数。所以CGI模块体积庞大。在其中找到正确的函数并不容易。
所以,基于速度和简单性方面的考虑,本章使用了CGI::Thin模块。
在开始创建CGI程序前,需要安装两个模块:
l CGI::Thin¾¾编码CGI数据。
l HTML::Entities¾¾文本到HTML的编码。
CGI程序开始代码为:
#!/usr/local/bin/perl –T
use strict;
use warnings;
use CGI::Thin;
use CGI::Carp qw(fatalsToBrowser);
use HTML::Entities;
同样,第一行为变幻行,它告诉Linux到哪里找到Perl,并在设置“Taint”标志的情况下启动它。
下一步,指定所使用的模块。
当CGI程序运行时,它做的第一件事是打印特殊字符串,它告诉web服务器下面是一个HTML页面:
print “Content–type: text/html\n”;
print “\n”;
CGI程序的难以调试众所周知(这一点将在“调用CGI脚本”一节中详述)。首先你必须调用一个调试程序,它打印表单中的所有数据。如果出错的话,这将起很大的帮助作用。
调试程序首先通过调用Parse_CGI函数从表单中得到数据。这个函数能够与web服务器通信并解码表单数据:
my %form_info = Parse_CGI();
函数以散列的形式返回表单数据,散列中的键来自于输入指令的NAME属性,值来自于用户的输入内容(在这里就是输入的文本项)或者VALUE属性(在这里是复选框或选项)。
例如,如果用户复选了由下面代码指定的空白:
<INPUT TYPE=”checkbox” NAME=”intern” VALUE=”1”>
Currently a member of an intern program</INPUT>
CGI程序将会看到:
$form_info{“intern”} = “1”;
为了调试,你需要打印出散列。可以通过一个小循环来完成:
foreach my $cur_key (sort keys %form_info) {
# WARNING: SEE BELOW
print “$cur_key = $form_info{$cur_key}\n”;
}
但是这样做存在一个问题。如果用户输入的数据包含HTML代码时,怎么办?例如:
This is a <B>Good</B> job.
在这种情况下,当打印这一字符串时,它将被解释成HTML字符串,结果是:
This is a Good job.
看到的并不是用户输入的准确内容。此时,事情并不是太糟糕,因为用户输入的是合法的HTML代码。但是用户也可能输入:
Will be finished in “< two weeks”.
因为文本中存在“<”,浏览器将把它看成一个HTML指令并试图执行它。
你需要解码输出,使得特殊字符不会导致发生可笑的事。例如,如果你想打印前一个注释,应该打印成:
Will be finished in “< two weeks”.
这就要用到HTML::Entities模块了。它提供了函数encode_entities将文本转换成HTML,然后就可以打印了。因此实际的打印循环是:
foreach my $cur_key (sort keys %form_info) {
print encode_entities($cur_key), “ = “,
encode_entities($form_info{$cur_key}), “\n”;
}
实际上,并不仅仅如此。有时,系统中同一个键可能有多个值¾¾例如,当你表单中有一个多选列表时。此时,Parse_CGI函数将散列的值设置成数组引用。
对此要作检查并进行正确的处理:
if (ref $form_info{$cur_key}) {
#.... 打印数组 @{$form_info{$cur_key}}
} else {
#.... 打印标量 $form_info{$cur_key}
}
注意,ref函数在标量是一个引用时返回true,在不是一个引用时返回undefined。
服务器与应用程序间的通信不仅通过CGI协议,还通过环境。所以打印了所有的CGI参数后,还要同时打印环境。
12.4.1 记录数据
debug函数之后就是程序主体,它记录下数据。这是一段简单易懂的Perl脚本,所以这里不作讨论。
唯一要注意的一件重要事情是所有的输出文件都命名为form.<time>,其中<time>为以秒计的系统时间。这保证了输出文件具有唯一的名字。(假定两个人不会在同一时间提交报告)
另一件要注意的事是文件名的创建与用户输入无关。也就是说,选择文件名时没有使用表单中的内容。这是一种安全措施。安全将在“安全”一节中作更详细的讨论。
12.4.2 编写反应
程序的最后一部分是编写反应,让用户知道数据已提交。
print <<EOF;
<H1>Thanks</H1>
<P>
Thank you for your time. Your information has been
recorded.
EOF
12.4.3 将各个部分放在一起
程序清单12-3是这个CGI脚本的完整版本。
程序清单12-3 record.pl
#!/home/sdo/local/bin/perl –T
use strict;
use warnings;
use CGI::Thin;
use CGI::Carp qw(fatalsToBrowser);
use HTML::Entities;
my $out_dir = “/tmp”;
########################################################
# 调试¾¾对web表单的输出调试信息
#
# 打印所有数据和环境
########################################################
sub debug()
{
print “<H1>DEBUG INFORMATION</H1>\n”;
print “<H2>Form Information</H2>\n”;
# –––––––– 打印表单信息 –––––
my %form_info = Parse_CGI();
foreach my $cur_key (sort keys %form_info) {
print “<BR>”;
if (ref $form_info{$cur_key}) {
foreach my $value (@{$form_info{$cur_key}}) {
print encode_entities($cur_key), “ = “,
encode_entities($value), “\n”;
}
} else {
print encode_entities($cur_key), “ = “,
encode_entities($form_info{$cur_key}), “\n”;
}
}
# –––––––– 打印环境 ––––––
print “<H2>Environment</H2>\n”;
foreach my $cur_key (sort keys %ENV) {
print “<BR>”;
print encode_entities($cur_key), “ = “,
encode_entities($ENV{$cur_key}), “\n”;
}
}
sub write_report()
{
# 表单中的信息
my %form_info = Parse_CGI();
# 当前时间,以秒计
my $time = time();
open OUT_FILE, “>$out_dir/form.$time” or
die(“Could not open $out_dir/form.$time”);
print OUT_FILE “Name: $form_info{name} Group: $form_info{group}\n”;
if ($form_info{work}) {
print OUT_FILE “Full Time Employee\n”;
}
if ($form_info{intern}) {
print OUT_FILE “Part of the Intern program\n”;
}
if (not defined($form_info{schedule})) {
print OUT_FILE “*** Did not select a schedule\n”;
} else {
print OUT_FILE “Schedule: $form_info{schedule}\n”;
}
print OUT_FILE “Comments:\n”;
print OUT_FILE “$form_info{comments}\n”;
close (OUT_FILE);
}
print “Content–type: text/html\n”;
print “\n”;
debug();
write_report();
print <<EOF;
<H1>Thanks</H1>
<P>
Thank you for your time. Your information has been
recorded.
EOF
12.5 调试CGI脚本
CGI程序难以调试。你常常只在屏幕上得到这样的信息“Internal Server Error”,对于所发生的事毫无提示。而且,将打印语句放在代码中的老办法也不起作用。你没有打印输出的屏幕。本节讲解一些可能出现的不同漏洞,以及处理它们的办法。
12.5.1 解决“Internal Server Error”
假如你试图执行一个CGI脚本,而服务器返回一条屏幕提示“Internal Server Error”或“Forbidden”。它表示CGI脚本不能运行或服务器无法识别它的返回信息。
为了解决这个问题,你应当检查以下几个方面:
1. 通过如下命令试试检查程序的语法:
perl –wcT script.pl
2. 确保可执行,而且每人可读懂。记住执行脚本的是服务器而不是你自己。这意味着执行这个帐号的Linux/UNIX用户并不是你。
3. 如果可能,请检查服务器的错误日志查看发生的一切。如果你使用的是最常见的web服务器Apache,那么日志的缺省位置是/var/log/httpd/error_log。(同样这个位置与具体的系统/安装路径有关)
如果以上办法失败,你可以尝试手工运行程序,或从服务器管理员那里获取额外的帮助。这将有助于解决大部分脚本可能产生的“软件错误”。
12.5.2 交互式调试
交互式调试有两种方式。第一种方式是模拟web服务器环境,第二种是利用web服务器来调试。
为了模拟服务器环境,本节将创建一个短小的Perl脚本来读取表单信息,将它编码,然后调用这个程序。
需要安装Parse_CGI模块来完成。这个模块提供了函数enurl。这个函数的作用与Parse_CGI相反,它带有一个散列,返回表示这个散列的编码字符串。
模拟函数开始时询问用户脚本的名字,然后以键/值对的形式获取表单数据。下一步,通过enurl函数编码字符串:
my $url_info = enurl \%cgi_info;
给CGI程序传递数据的最简单途径之一是通过环境。通过设置一些关键环境变量并执行程序本身来模拟:
# 设置最小数量的环境变量
$ENV{QUERY_STRING} = $url_info;
$ENV{REQUEST_URI} = “$prog?$url_info”;
$ENV{REQUEST_METHOD} = “GET”;
exec(“perl –d $prog”);
一个典型运行过程如下所示:
perl debug.pl
Script name: /home/httpd/cgi–bin/record.pl
Enter key=value
(End with a blank line)
> name=oualline
> group=Software
> schedule=early
> version=1.0
> work=1
>
Loading DB routines from perl5db.pl version 1.0402
Emacs support available.
Enter h or `h h’ for help.
main::(/home/httpd/cgi–bin/record.pl:33):
33: print “Content–type: text/html\n”;
DB<1>
现在就可以正常调试CGI程序了。程序清单12-4是这个调试程序的完整版本。
程序清单12-4 debug.pl
use strict;
use warnings;
use CGI::Enurl;
my %cgi_info = (); # 创建的CGI信息
print “Script name: “;
my $prog = <STDIN>;
chomp($prog);
print “Enter key=value\n”;
print “(End with a blank line)\n”;
# 得到对,其样式 name=value
while (1) {
print “> “;
my $line = <STDIN>;
chomp($line);
if ($line eq “”) {
last;
}
if ($line !~ /^([^=]*)\s*=\s*(.*)$/) {
print “ERROR: Could not understand $line\n”;
next;
}
$cgi_info{$1} = $2;
}
# 编码CGI信息,用于模拟
my $url_info = enurl \%cgi_info;
# 设置最小数量的环境变量
$ENV{QUERY_STRING} = $url_info;
$ENV{REQUEST_URI} = “$prog?$url_info”;
$ENV{REQUEST_METHOD} = “GET”;
exec(“perl –Td $prog”);
12.5.3 服务器启动的调试器
调试CGI程序的另一种途径是让服务器启动调试器。这可能有一些难度,因为程序启动时没有终端与之相连。
如果你使用的是X Windows系统,可以通过一些技巧将调试器连接到显示窗口。第一步是找到显示的名字。通过下面的命令来找到:
echo $DISPLAY
:0.0
这里使用的显示是:0.0。下一步是创建shell脚本,在指定窗口上启动调试。
如果你启动的是ptkdb调试器,对应的脚本如程序清单12-5所示。
程序清单12-5 record.ptkdb
#!/bin/sh
DISPLAY=:0.0
export DISPLAY
exec perl –wTd record.pl “$*”
第一行是变幻行,告诉系统这是一个shell(sh)脚本。下面两行告诉系统使用哪个显示。最后,使用调试器执行真实的程序。
脚本必须放在CGI目录,而且是可执行的。
下一步,创建一个web表单,调用调试包装程序(record.ptkdb),而不是真实的脚本(record.pl)。
最后需要告诉X Windows系统你需要从一个CGI程序与之相连,而且要接受来自web服务器的连接。通过下面的命令来完成:
xhost +
现在当你提交调试表单时,脚本record.ptkdb就会运行。它将输出重定向到显示(这里的显示是:0.0),然后启动对脚本的调试器。
进行服务器调试的步骤总结:
1. 找到正在运行的显示(echo $DISPLAY)。
2. 创建设置显示和启动调试器的包装器。
3. 将包装器放在CGI目录。
4. 创建一个表单调用包装器。
5. 允许其它系统连接到显示(xhost +)。
6. 运行调试表单。
使用几乎完全相同的技术来调试应用命令行调试器的程序。在这种情况下,使用xterm程序来创建一个新的窗口用于调试。程序清单12-6演示了实现的方式。
程序清单12-6 record.debug
#!/bin/sh
exec /usr/bin/X11/xterm –display :0.0 –e perl –wTd record.pl “$*”
请注意,当你使用这个调试技术时,CGI程序的所有输出都被发送到终端,而不是web服务器。结果导致你看到了所有内容,但web服务器得到一条“Internal Server Error”的信息。
12.6 安全
web表单本身存在一系列安全问题。幸运的是,操作系统、web浏览器以及Perl针对黑客均提供了一些安全措施。但这并不意味着你可以忽视这些安全问题。
第一道防线来自操作系统和web浏览器。web浏览器在特殊的用户账号下运行,一般名字为www或类似名字。这意味着所有的CGI脚本只可以访问这个用户可获取的数据,只可以销毁对这个用户来说是可写的文件。
不过,有时这可能导致问题的产生。假如你希望CGI程序创建的数据由你自己拥有而不是由用户www拥有。Linux和UNIX通过一种称做setuid bit的东西来解决这一问题。如果脚本设置了这个位,脚本只对拥有它的用户才运行,而不是对调用它的所有用户运行。
为了创建setuid脚本,必须运行正确的chmod命令:
chmod u+s script.pl
这个命令告诉系统当script.pl被运行时,就像是你自己在运行上它一样[1]。
12.6.1 Taint模式
下一道防范黑客的防线是Perl的Taint模式。在程序的第一行通过-T开关打开这一模式。在Taint模式,由外界提供的任何东西都被看作受到了污染,如果以一种不安全方式使用,就会产生错误。
比如,你有一个让用户提交名字和密码的表单,而且你希望外部程序来验证它。下面就是完成这一工作的一段不安全代码:
my $user = $form_data{USER};
my $password = $form_data{PASSWORD};
my $status = system(“validate_user $user $password”);
if ($status == 0) {
print “User is good\n”;
} else {
print “User is bad\n”;
}
这个程序的问题出在哪儿呢?问题在于用户可以提供任何形式的用户名。有一些是合理的,如sam,有一些名字可以借以攻击系统,如sam ; rm –rf /。
第二种名字导致系统执行如下命令:
validate_user sam ; rm –rf /
因为分号为shell的命令分隔符,所以,这实际上是两个命令:validate_user和rm –rf。这并不是我们所期望的。
一般情况下,Perl的Taint模式阻止你在系统或open函数中使用任何未检查用户输入。
例如,在Taint模式打开情况下,在一个系统调用中使用受污染用户输入导致如下错误:
Insecure dependency in system while running with –T switch
Âat passwd.pl line 7, <> line 1.
直接赋值不会去掉数据中的污点。不过,如果从受污染数据中提取字符串得到的结果是未污染的。例如:
$tainted_data =~ /^(\s+)$/;
my $untainted_data = $1;
来自环境的也看作是未污染的。特别是,如果路径受污染,Perl不会执行系统命令。为了去掉污点,你需要亲自设置路径,并从环境中去掉可能影响shell的任何变量:
$ENV{PATH} = “/bin:/usr/bin:/usr/X11R6/bin”;
delete @ENV{‘IFS’, ‘CDPATH’, ‘ENV’, ‘BASH_ENV’};
12.6.2 Perl程序是如何崩溃的
实践是最好的老师。但谁也不愿意真正去体验安全方面的灾难。所以,有必要了解要避免的几个方面。
首先,记住用户名、文件名和其它用户输入可能包含特殊字符。前面已经见过sam ; rm –rf /这样的用户名是如何产生破坏的。
不要假定文件名只包含合法字符。例如,假如你的脚本设计用于取回树/home/status/data中的文件,编写代码如下:
chdir(“/home/status/data”);
open IN_FILE, “<$user_supplied_file_name”;
用户提供的文件名可能简单(如status.txt),可能是设计用于获取数据的非法字符串(如../../../etc/passwd)。对于第二种情况,这样的文件定义就会赋予用户访问系统密码文件的权限。这样的文件对黑客是有用的。
Code Red蠕虫病毒就是使用这一技术的变种侵入Microsoft IIS服务器中。服务器只是简单地检查.../..这样的字符串,但可以采取多种途径来隐藏这种使用Unicode的路径,蠕虫病毒正是利用了这一点。
另一个错误是,认为隐藏的输入字段没有被用户修改。记住,用户可以编写自己的web页并指向你的CGI程序。许多在线商店都发现了这一问题。商店将物品的价格放在生成web页上一个名叫PRICE的隐藏字段中。黑客入侵后看到这一行:
<INPUT TYPE=”hidden” NAME=”price” VALUE=”298.99”>
并在给商店提交表单前进行镜像编辑:
<INPUT TYPE=”hidden” NAME=”price” VALUE=”2.99”>
无庸置疑,他们以非常便宜的价格得到了这个数码相机。
12.7 Cookie
编写CGI程序的一个弊病是每次运行都是独立的。难以将一次运行的信息带到下一次运行中。
一个解决方案是使用隐藏INPUT标签。另一个解决方案是使用Cookie。Cookie是服务器发送给浏览器的命名字符串。浏览器然后才能对请求作出反馈。
比方你想给浏览器发送一个Cookie。首先需要CGI::Thin::Cookies模块。(安装CGI::Thin时就已经安装了)然后使用Set_Cookie函数发送Cookie:
use CGI::Thin::Cookies;
#.....
Set_Cookie(
NAME => “Username”,
VALUE => $user,
EXPIRES => “+14d”
);
这个例子发送了一个名叫“username”的Cookie,它的值为用户的名字。Cookie在14天后过期。
在输出HTML标题行(“Content: text/html”行)前必须调用Set_Cookie。
调用Parse_Cookies函数得到返回的Cookie。这个函数返回一个包含浏览器发送给服务器的所有Cookie的散列。(注意:仅包括脚本启动时用到的Cookie,不包括脚本运行期间发送的Cookie)
因此在这个例子中,发送了NAME Cookie后,可以通过如下代码在下一次运行时获得返回的Cookie:
use CGI::Thin::Cookies;
my %cookies = Parse_Cookies();
if (defined($cookies{NAME})) {
print “<P> Hello $cookies{NAME}\n”;
}
第15章“综合运用”将对Cookie作更多的讨论,在那时我们要创建一个真正的CGI程序,允许用户访问一个磁盘存货系统。
12.8 小结
CGI编程一方面相当简单,一方面又极富技巧性。编写CGI程序相当容易。但让它开始工作可能需要技巧了。我希望本章提供的技术与工具能使CGI脚本的创建工作变得容易一些。
12.9 练习
1. 创建一个打印当前系统状态的脚本。报告的内容凭你的想像而定,但某些项目要包括,如当前日期、每个文件系统的磁盘剩余空间、系统已经运行的时间等等。
2. 编写一个CGI脚本用于调试。这个脚本打印所有提供给它的表单数据以及环境变量的值。
3. 编写一个表单,允许用户登记他的产品。表单应该让用户输入名字、地址和电话号码,并保存起来,市场部以后可以使用这些信息编制邮件列表。
4. 困扰系统管理员的一个问题是,许多用户要求恢复某个文件。创建一个CGI系统来自动完成这个任务。包含一个允许用户提交请求(请恢复这个文件)的表单,以及供系统管理员使用的报告表单(这里就是已经恢复的文件)。你可以根据需要添加其它报告数据和表单。
5. 创建一个在线测试系统。这个系统将询问用户一系列问题,对结果进行打分(让用户知道哪儿出错了),并记录下这位用户的得分等级。
12.10 资源
12.10.1 在线文档
l perldoc perlsec¾¾Perl安全信息页。这个文档页提供了有关Taint模式的大量信息。
12.10.2 模块
l CGI::Thin¾¾对进入CGI程序(小型CGI版本)的信息进行解码。
l CGI::Thin::Cookies¾¾处理Cookie的发送和接收(CGI::Thin模块的一部分)。
l CGI::Carp¾¾拦截die以及CGI程序可能发生且对用户显示错误信息的不正常情况。
l CGI¾¾能完成CGI程序希望完成的任何事,同时也能完成一些你并不希望做的事。参议对于几乎所有的应用程序都使用更小、更简单的CGI::Thin模块。
l CGI::Enurl¾¾按浏览器给用户发送表单的相同编码方式对URL进行编码。
l Taint¾¾提供tainted函数,用于检测某变量是否被污染。
12.10.3 网站
l http://www.securityfocus.org¾¾一般性系统安全优秀站点。
[1] 译者注:chmod是一个非常重要的命令,用于改变文件或目录的访问权限。用户用它控制文件或目录的访问权限。u 表示“用户(user)”,即文件或目录的所有者;s 在文件执行时把进程的属主或组ID置为该文件的文件属主。“u+s”设置文件的用户ID位。