MySQL、CSS 和 Perl 的 Web 报告
在 2005 年 3 月号的 Linux Journal 中,我使用了 Maypole 用仅 18 行 Perl 代码创建了一个基于 Web 的数据库应用程序。Maypole 提供的功能令人印象深刻,但在一个重要领域除外:报告。因此,我开始研究从我的足球俱乐部系统生成报告的技术。我的目标是提供一组可以通过 Web 界面执行的标准报告。
Web 报告可以使用许多服务器端编程技术(例如 PHP、JSP、Perl 脚本等)以多种方式生成。独立的桌面报告工具也可用,甚至可以使用 OpenOffice.org 对 MySQL 数据库进行报告。但是,由于我的报告要求很简单,我想尽量减少我的智力投入。我不介意花时间编写我需要用来生成报告的 SQL 查询。编写完成后,我希望我的 SQL 查询生成一个 HTML 结果表。
当然,我可以使用 Perl 通过 DBI 和 DBD::mysql 模块来做到这一点,手工编写程序代码将查询发送到数据库。然后我可以再用更多代码对结果进行后处理,最终再编写更多代码来创建表格。对于我简单的要求,这感觉工作量太大了。我真正想要的是一个快速而粗糙的解决方案。在本文的其余部分,我将详细介绍我设计的 Web 报告解决方案。
在浏览 Paul DuBois 的优秀著作 MySQL Cookbook 时,我发现了一个命令行选项,可以将命令行查询的结果转换为 HTML 表格(配方 1.23,第 33 页)。例如,考虑以下命令行
mysql -e "select name from player" \ -u manager -ppwhere CLUB
当调用时,它产生以下文本输出
+-------------+ |name | +-------------+ |Robert Plant | |Tim Finn | |James Taylor | |Bryan Adams | |Ian Gillen | |Mick Jagger | |Neil Young | |Bob Dylan | +-------------+
这些结果不仅显示了足球俱乐部数据库中所有球员的姓名,而且似乎还表明该俱乐部的球员以一些著名的民谣和摇滚歌手的名字命名。当使用 HTML 创建选项重新运行时,如下所示
mysql -H -e "select name from player" \ -u manager -ppwhere CLUB
上面的命令行产生以下内容,请相信我,这是一个 HTML 表格
<TABLE BORDER=1><TR><TH>name</TH></TR><TR><TD> Robert Plant</TD></TR><TR><TD>Tim Finn</TD></TR> <TR><TD>James Taylor</TD></TR><TR><TD>Bryan Adams </TD></TR><TR><TD>Ian Gillen</TD></TR><TR><TD> Mick Jagger</TD></TR><TR><TD>Neil Young</TD></TR> <TR><TD>Bob Dylan</TD></TR></TABLE>
可以将 SQL 查询放入文件中,然后在命令行上引用该文件。例如,假设上面的查询在名为 name.sql 的文件中,则此命令行产生相同的 HTML 表格
mysql -H -u manager -ppwhere CLUB < name.sql
了解了这么多,我认为如果我可以想出一种从 Web 界面发出 HTML 生成命令行的方法,我就离提供我的 Web 报告解决方案不远了。因此,我编写了一个小的 CGI 脚本(用 Perl 编写)来为我执行命令行。
我的简单 CGI 脚本采用的策略很简单:在确定要执行的查询名称后,构造一个命令行,然后由 CGI 脚本发出。执行命令行产生的任何结果都放在 CGI 脚本生成的 HTML 页面的正文部分内。
在通常的 Perl 启动行之后,runquery.cgi 脚本首先定义一系列常量值
#! /usr/bin/perl -w use strict; use constant MYSQL => '/usr/bin/mysql'; use constant USERID => 'manager'; use constant PASSWD => 'pwhere'; use constant DBNAME => 'CLUB';
您计算机上 MySQL 客户端的位置可能与我的位置不同,因此如果需要,请更改 MYSQL 常量值。另请注意,我硬编码了数据库用户 (USERID)、密码 (PASSWD) 和要查询的数据库 (DBNAME) 的值。虽然这可能不是最佳实践,但我将通过说这是我的快速而粗糙的解决方案的脏乱部分来解释它。定义常量后,我指示我将使用 Perl 的 CGI 编程技术的标准接口
use CGI qw( :standard );
然后定义了两个 Perl 标量,它们的值取自从 Web 界面传递到 CGI 脚本的任何参数。第一个参数名为 query,用于标识要使用的 SQL 文件,而第二个参数名为 title,用于提供在显示结果时使用的报告标题
my $query = param( 'query' ); my $title = param( 'title' );
然后,脚本创建通过 MySQL 客户端程序运行查询的命令行。请注意,Perl 的点运算符用于连接字符串
my $cmdline = MYSQL . ' -H -u ' . USERID . ' -p' . PASSWD . ' ' . DBNAME . "< $query ";
然后,脚本开始构建 HTML 页面。header 函数生成正确的 Content-Type 标头,start_html 函数开始使用为页面标题提供的值创建 HTML 页面
print header; print start_html( -title => $title );
下一行代码使用 Perl 的 qx 运算符执行命令行,并将执行产生的任何输出返回给名为 $results 的变量
my $results = qx/ $cmdline /;
脚本的其余部分向 Web 页面添加一个 HTML 3 级标题,以及查询结果和一个指向报告页面的 HTML 链接。end_html 函数完成 HTML 页面生成并结束脚本
print "<h3>$title</h3>"; print $results; print p, "Return to the ", a( { -href => "/Club/Reports.html" }, "List of Reports" ); print end_html;
要运行脚本,您需要做两件事:将脚本放在 Web 服务器可以找到它的位置,并将 SQL 查询放入文件中。在我的运行 Apache 2 的 Fedora Core 3 系统上,/var/www/cgi-bin/ 目录用于保存 Web 服务器的 CGI 脚本。因此,我只需将 CGI 脚本复制到该位置并使其可执行
cp runquery.cgi /var/www/cgi-bin/ chmod +x /var/www/cgi-bin/runquery.cgi
上面的目录可能不是您的发行版用于 Web 页面的位置,因此请务必先检查。至于查询,这是文件 conditions.sql 的内容
select player.name as 'Player', condition.name as 'Medical Condition' from player, condition where player.medical_condition = condition.id and player.medical_condition != 1;
上面的 SQL 查询联接 player 和 condition 表,以便列出每个球员的姓名及其医疗状况(假设他有)。此查询文件还需要复制到 Web 服务器上的 CGI 目录
cp conditions.sql /var/www/cgi-bin/
要从 CGI 脚本执行查询,请在浏览器的地址栏中键入以下内容,替换localhost为您的 Web 服务器的名称
http://localhost/cgi-bin/runquery.cgi? \ title=Results&query=conditions.sql
此 URL 生成图 1 中所示的输出,尽管有点简陋,但看起来还可以,但可以更好看。
为了生成外观更漂亮的报告,我创建了一个小型级联样式表 (CSS),名为 reports.css,以改善生成的报告的整体外观
body { font-family: sans-serif; } table { font-family: sans-serif; background-color: LIGHTYELLOW; } table th { background-color: LIGHTCYAN; font-size: 75%; } h3 { font-family: sans-serif; color: BLUE; }
就样式表而言,我的非常简单。我为我的正文中的文本声明了一种字体,然后我调整了 HTML 页面上任何表格的字体和背景颜色。表格标题以用户正常文本大小的 75% 显示,背景颜色与表格中的数据不同。然后我声明我的 3 级标题为蓝色。
CSS 文件需要复制到 Web 服务器的根目录中,以便我的 Web 页面可以找到它
cp reports.css /var/www/html
要使用 CSS 文件,我将 runquery.cgi 中的 print start_html 行更改为引用样式表,如下所示
print start_html( -title => $title, -style => { -src => "/reports.css" } );
重新加载查询会生成图 2 中所示的输出。它可能不会为我赢得 Web 设计奖,但它确实比图 1 中所示的普通结果好看得多。
我解决方案的这一部分很容易。我只需要一个简单的 Web 页面来描述报告列表。与生成的报告一样,我使用简单的样式表来改善报告页面的外观。以下是我使用的 HTML
<HTML> <HEAD> <TITLE>Soccer Club Reporting System</TITLE> <LINK rel="stylesheet" type="text/css" href="/reports.css" /> </HEAD> <BODY> <H3>Soccer Club Reporting System</H3> Choose from one of these reports: <OL> <LI>List players that have a <a href="/cgi-bin/runquery.cgi? title=Players with a Medical Condition& query=conditions.sql">Medical Conditions</a> <LI>List all players, <a href="/cgi-bin/runquery.cgi? title=Listing of all Players (Youngest First)& query=desc_dob.sql">youngest first</a> </OL> Return to the <A HREF="/Club">Soccer Club</A> database system. </BODY> </HTML>
如上所示,每个报告都使用两个参数执行:title,它提供报告描述;query,它标识要通过 MySQL 运行的 SQL 查询文件。创建 Web 页面后,我将其复制到足球俱乐部网站的根目录中
cp Reports.html /var/www/html/Club/
加载到 Web 浏览器后,报告 Web 界面的外观如图 3 所示。
至此,我认为我完成了。我有一个简单的 Web 界面,可以访问标准报告生成机制。如果我编写更多查询,我可以将它们放入自己的 SQL 查询文件中,将文件复制到我的 cgi-bin 目录,并更新我的 HTML 报告 Web 页面以根据需要调用查询。我的解决方案快速而粗糙,并且足够好。
或者真是这样吗?
我的解决方案的安全性非常非常差。我需要担心两件事,保护我的 CGI 脚本和 SQL 查询文件免受用户篡改,以及保护我的系统免受 CGI 脚本的攻击。
当谈到篡改 CGI 脚本和 SQL 查询文件时,问题在于,默认情况下,系统上运行 Web 服务器的任何登录用户都可以读取所有文件;一个简单的 cat 或 less 命令就可以做到这一点。任何用户都可以查看 runquery.cgi 内部并显示用于访问数据库的用户名和密码,这不好。
Apache httpd.conf 配置文件中的 User 和 Group 指令指示 Apache Web 服务器在其下运行的用户和组。在我的计算机上,此用户和组设置为 apache。了解这一点后,我发出了以下命令,以确保 CGI 脚本和 SQL 查询文件的内容归 apache 用户所有,并且它们只能由同一 apache 用户读取和写入。这阻止任何其他用户(当然 root 除外)检查其内容
cd /var/www/cgi-bin chown apache:apache * chmod 600 * chmod 700 *.cgi
第一个 chmod 命令确保 cgi-bin 目录中的文件只能由 apache 用户读取和写入。第二个 chmod 命令为任何 CGI 脚本打开可执行位,但仅适用于文件的所有者。有了这些简单的预防措施,我的解决方案现在可以防止用户篡改。
上面的 chmod 命令行保护文件免受登录到系统的其他用户的攻击,但我的解决方案仍然很脆弱。不幸的是,任何可以通过任何 Web 浏览器访问 Web 服务器的用户都可以利用它。例如,考虑如果将以下 URL 发送到 CGI 脚本会发生什么
http://localhost/cgi-bin/runquery.cgi? \ title=Ha!&query=conditions.sql | cat runquery.cgi
CGI 脚本的内容出现在浏览器中,发出者可以轻松读取文件中包含的数据库名称、用户名和密码。这已经够糟糕的了,但想象一下,如果cat runquery.cgi上述 URL 中的管道被替换为此
cat /etc/passwd
或可能灾难性的
rm -rf /
已编写的 CGI 脚本的问题在于,它盲目地信任发出者不会篡改 URL。通过简单地将管道符号和任何其他 shell 命令行添加到 URL,发出者会利用这个设计不良的 CGI 脚本,有效地在 Web 服务器上执行发出者选择的其他命令。通过将未更改的查询字符串传递给操作系统执行,CGI 脚本使此类漏洞非常容易被利用。
值得庆幸的是,Perl 有一种特殊的运行模式可以提供帮助,它被称为污点模式。大多数关于 Perl 的书籍都描述了污点模式,Christiansen 和 Torkington 的 Perl Cookbook 第二版提供了一个方便的入门指南(配方 19.4,第 767 页)。通过启用污点模式,Perl 解释器被指示不要信任源自脚本外部的数据。由于数据不受信任(它是“污点的”),Perl 不允许您以不安全的方式使用数据,否则会引发运行时异常。
我可以通过更改 CGI 脚本的第一行以包含污点模式开关来启用 Perl 的污点技术
#! /usr/bin/perl -wT
当我重新加载被篡改的 URL 时,生成的 HTML 页面是空的,并且 Apache 的 error_log 已附加了一个不安全的依赖项错误。这是 Perl 告诉我脚本由于污点错误而失败的方式。显然,由于脚本以这种方式失败,它不再对系统构成安全威胁。但是,它也不再执行其设计目的,这使得它几乎无法使用。为了使脚本再次可用,我们需要使用 Perl 的正则表达式技术来消除输入数据的污点。这个想法很简单:通过定义一个表示安全数据的模式,可以将该模式应用于污点数据,并且(假设模式匹配)任何结果都是未污点的并被认为是安全的。对于 CGI 脚本,有两个数据输入,query 和 title。我将以下正则表达式添加到 CGI 脚本以消除输入数据的污点
$query =~ /^([-\w]+\.sql)$/; $query = $1; $title =~ /^([\w:.?! ]+)$/; $title = $1;
第一个正则表达式匹配一个字符串,该字符串包含连字符或单词字符的任意组合,后跟句点和字母 s、q 和 l。任何其他内容都不匹配,并且被认为是可疑的。如果发生匹配,Perl 会将匹配项记住在 $1 匹配变量中,然后将其分配回现在未污点的 $query。对于标题,该模式允许单词字符以及正则表达式方括号中包含的字符的任意组合。同样,当成功匹配时,$title 变量将变为未污点。
由于 CGI 脚本执行外部可执行文件(即 MySQL 客户端),因此还需要消除环境路径的污点。这可以通过将环境中的 PATH 变量设置为安全目录列表来完成,如下所示
$ENV{'PATH'} = "/usr/bin";
对 runquery.cgi 脚本进行这些更改后,它再次可用。除了快速而粗糙且可以防止用户篡改外,我的解决方案不再对我的系统构成潜在的安全威胁。
为了将我的简单报告界面链接到我的足球俱乐部应用程序中,我更改了 custom/frontpage 模板以包含一个额外的列表项,该列表项引用报告 Web 页面
<ul> [% FOR table = config.display_tables %] <li> <a href="[%table%]/list">Work with the [%table %] data</a> </li> [% END %] <li>Work with the <a href="Reports.html"> Reports System</a> </ul>
当应用程序加载到浏览器中时,链接将作为初始 Maypole 菜单的一部分出现,如图 4 所示。我的基于 Web 的报告系统简单、安全且易于扩展。我现在需要做的就是编写更多 SQL 查询。
Paul Barry (paul.barry@itcarlow.ie) 在爱尔兰卡洛理工学院任教。有关他教授的课程以及他撰写的书籍和文章的信息,可以在他的网站 glasnost.itcarlow.ie/~barryp 上找到。