12章  CGI编程

我们现在看到的许多交互web页后面的支持语言就是Perl。只要你往表单中输入数据或从交互报告中获取信息,你大概就是在与Perl打交道。

交互web页使用了一种称做CGICommon Gateway Interface,公共网关接口)的API来处理用户与应用程序间的交互。因为Perl具有优秀的字符串处理功能,所以,它是编写web应用程序的理想语言。

本章讲解CGI是如何工作的,以及如何编写简单的CGI程序来增强你的网站。

本章面向UNIXLinux系统,因为这是大部分服务器运行的平台。

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 WorldCGI版本)

常见问题:错误目录或配置错误的服务器?

有时,当你输入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股票报价。

不过,如果你不希望信息暴露在屏幕的顶端,就使用POSTGET方法发送的数据长度还限制在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”,如果它被设置则返回值1CHECKED属性告诉浏览器这一个复选框最初被复选。

第二个复选框没有这样的属性,所以按缺省即没有复选。

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属性告诉系统空白使用什么标识符。属性CLOSROWS指定文本框的宽度和高度。

文本框的初始文本放在<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 “&lt; 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拥有。LinuxUNIX通过一种称做setuid bit的东西来解决这一问题。如果脚本设置了这个位,脚本只对拥有它的用户才运行,而不是对调用它的所有用户运行。

为了创建setuid脚本,必须运行正确的chmod命令:

chmod u+s script.pl

这个命令告诉系统当script.pl被运行时,就像是你自己在运行上它一样[1]

12.6.1  Taint模式

下一道防范黑客的防线是PerlTaint模式。在程序的第一行通过-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_userrm –rf。这并不是我们所期望的。

一般情况下,PerlTaint模式阻止你在系统或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标签。另一个解决方案是使用CookieCookie是服务器发送给浏览器的命名字符串。浏览器然后才能对请求作出反馈。

比方你想给浏览器发送一个Cookie。首先需要CGI::Thin::Cookies模块。(安装CGI::Thin时就已经安装了)然后使用Set_Cookie函数发送Cookie

use CGI::Thin::Cookies;

#.....

Set_Cookie(

    NAME => “Username”,

    VALUE => $user,

    EXPIRES => “+14d”

);

这个例子发送了一个名叫“username”的Cookie,它的值为用户的名字。Cookie14天后过期。

在输出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置为该文件的文件属主。“us”设置文件的用户ID位。