CGI:安全第一
随着当前 Web 的热潮,系统管理员的噩梦变成了现实:系统上的每个用户都想要拥有一个主页,并且想要进行 CGI 编程。上网的冲动太强烈了,无法阻止他们。如果他们有一些基本的安全知识,情况就不会那么糟糕,但事实并非如此:这些用户中的大多数人都是在 MS-DOS 环境下长大的,在 MS-DOS 中,不安全是事实上的标准。他们对您的系统持有一种危险的态度。
CGI 编程的一个主要问题是写入文件;Web 服务器不是以编写 CGI 脚本的用户身份运行的——当它尝试在文件中写入内容时,会收到权限被拒绝的错误。降低该文件的权限不是一个解决方案——这简直是灾难性的。
您可能知道,Web 服务器(通常称为 httpd)可以由任何用户运行。但是,如果您希望服务器监听 HTTP 端口(根据 STD 2,互联网标准“分配号码”的第 80 端口,目前为 RFC 1700),则必须以 root 用户身份启动它。通常,这是在启动时从 /etc 中的 rc 文件之一完成的。由于以 root 用户身份运行 Web 服务器是对安全性的严重破坏,因此所有 Web 服务器都可以配置为以不同的用户身份运行,例如用户“www”。
您通常会看到一个以 root 用户身份运行的服务器,它负责打开 80 端口并 fork 出自身的副本,该副本以 www 身份运行。它启动尽可能多的服务器来处理您的负载(您通常可以在配置文件中设置服务器的最小和最大数量)。以 root 用户身份运行的服务器不执行 HTTP 操作——这由以 www 身份运行的子服务器完成。
当每个人都可以编写 CGI 脚本时,运行 CGI 脚本存在一个固有的安全风险:其他用户可以读取这些脚本。即使您删除其他人的读取权限,并使脚本仅对服务器组可读和可执行,其他人也可以编写一个脚本来读取您的脚本的源代码。如果程序的源代码保持安全很重要,您就不应该编写脚本。编译后的程序不必是可读的即可执行,而脚本必须既可读又可执行。
在我们继续讨论真正的问题之前,请先考虑一下以 www 身份运行服务器的一些后果。考虑以下脚本
#!/bin/sh cp /bin/sh /home/cracker/bin/sh chmod 4755 /home/cracker/bin/sh cat << EOF Content-type: text/plain There is no such thing as a secure server! EOF
您现在创建了一个 suid www shell,任何黑客都可以使用它!因此,重要的是用户 www 没有主目录,并且不拥有任何重要的文件。另一个臭名昭著的伎俩是向所有服务器发送 STOP 信号;这真的很阴险(你知道为什么吗?)。如果您认为这些类型的脚本是不可接受的,您应该禁止系统上的所有人(除了少数受信任的用户)进行 CGI 编程。
应该清楚的是,以 root 用户身份运行所有服务器绝对是不可行的。这就像从密码文件中删除 root 的密码条目一样。如果您的所有服务器都以 root 用户身份运行,请立即冲到您的系统管理员那里纠正这个问题,并且在问题得到纠正之前不要离开。如果他很难说服,好吧,您拥有 root 权限,也许您应该给他演示一下您是如何纠正他的错误的!
当用户 foo 想要在文件中写入内容时,麻烦就开始了。好吧,仅仅为了论证,我们将在文件 ~foo/www/access 中增加一个访问计数器。例如,看看脚本 counter.sh(使用 Bourne 脚本进行 CGI 编程不是很常见,但它确实可以完成工作)
#!/bin/sh ACCESS= ACCESS= rm /home/foo/www/access echo $ACCESS > /home/foo/www/access cat << EOF Content-type: text/plain You are number $ACCESS to visit this page! EOF
一个不错的小脚本,但有一个问题:它不起作用。如果 foo 执行它,它就可以工作,但唉,www 是运行脚本的人。用户 foo 可能会立即通过输入:chmod 666 access 来降低其安全性。并且由于他注意到他的默认权限总是错误的,他确保他定期执行 chmod 666 *——没有意识到设置 UMASK 的选项。不要认为这不会在现实生活中发生——我的一些同事就是这样做的。请放心,他们都是忠实的 MS-DOS 用户。
他们中的一些人了解到,如果他不单独更改文件,而是更改目录,则更容易(当我听到这个时,我几乎从房间里跳了出来):chmod 777 . 他没有意识到写入成功了,因为守护进程能够删除文件访问。但不仅仅是 www 可以做到这一点——任何人都可以做到!
快速输入 chmod 1777 可能看起来是正确的做法。别相信。的确,它不是那么糟糕,但仍然,系统上的任何人都可以使用 CGI 脚本删除该文件。
现在是时候来点不那么令人沮丧的事情了。你能做得对吗?当然可以——这只需要一点努力。问题是您真的不希望您的 CGI 脚本由像 www 这样的陌生人执行。您希望您的脚本以您自己的身份运行!为了以其他用户身份执行程序,发明了 suid 和 sguid。
如果您使程序 suid(或 sgid),您将以拥有该文件的用户(或组)身份运行它。这正是您最初想要的。所以您使您的脚本 suid。但在 Linux 下仍然不起作用。为什么?因为 suid 脚本是不安全的,并且 Linux 内核完全拒绝 suid 脚本(查看 comp.os.unix FAQ,了解一些令人震惊的成为 root 用户的方法)。
但是 suid 程序可以安全使用。这只意味着您必须编写一个包装器,一个小型的 C 程序 counter.c,您将其设置为 suid
#include <stdlib.h> void main(int argc, char *argv[]) { exit(system("/home/foo/www/bin/counter.sh")); }
这种 suid 方法总是有效吗?不——许多系统管理员将 /home 挂载为 nosuid,这意味着 suid 位永远不会被遵守。为什么系统管理员要这样做?好吧,您注意到了本文中的第一个脚本,该脚本创建了一个 suid shell?如果 /tmp 挂载为 nosuid,那将不起作用。许多系统管理员仅将那些必须包含 suid 程序的文件系统挂载为 suid,将所有其他文件系统挂载为 nosuid;例如,/home 不需要 suid 程序即可使系统运行,因此挂载 nosuid 是一种预先防范措施。
但还有另一种方法。这有点像霰弹枪编程,但您可以让 sendmail 和 procmail 为您完成脏活。它是如何工作的?基本上,您让守护进程向您发送邮件,该邮件将指示您执行某项任务。接收 sendmail 将在您的 ~/.forward 中查找,并看到您希望您的邮件由 procmail 处理,procmail 将为您执行更新脚本。让我首先给您 CGI 脚本
#!/bin/sh ACCESS= ACCESS= cat <<EOF Content-type: text/plain You are number $ACCESS to visit this page! EOF echo "Subject: access.sh 1928397071" | sendmail foo
首先是一些关于邮件的说明
此邮件没有正文,这是 RFC 822(描述邮件消息格式的互联网标准)允许的。如果您想包含正文,则应在正文前加上一个强制性的空行。
在主题中,我们有一个脚本部分 access.sh,它将提示要运行哪个脚本。它有一些魔术 cookie (1928397071),以防止您处理意外触发的电子邮件。如果您希望这是一个真正安全的 cookie,您不应该将此行编写为脚本。
接下来是 ~/.forward
"|IFS=' '&&exec /usr/bin/procmail -f-||exit 75 #bar"
程序 procmail 需要在您的文件 ~/.procmailrc 中查找(如果您想使用 cookie 进行安全保护,则不应将其设置为全局可读)
LOGFILE=$HOME/log/procmail :0 b * ^From www * ^Subject: access.sh 1928397071 |$HOME/www/bin/access.sh
因此,如果守护进程发送一封主题为 access.sh 1928397071 的邮件,procmail 将为您运行 access.sh
#!/bin/sh ACCESS= ACCESS= rm /home/foo/www/access echo $ACCESS > /home/foo/www/access
任务完成:我们已安全地将内容写入文件。有什么陷阱吗?当然,肯定有。
首先,您需要确保当两个人尝试访问您的页面时,他们不会搞砸您的文件。您需要某种文件锁定机制(查看 flock,它在 Perl 中也可用)。
其次,CGI 脚本中的 sendmail 是非阻塞的:它不会等待更新实际完成。在大多数情况下,这无关紧要,因为您只是在进行简单的更新。否则,您可以随时尝试实现 Perl 客户端-服务器模型。
不要选择简单但不安全的选项;保持安全。您的系统管理员会感激不尽的。
Hans de Vreught (J.P.M.deVreught@cs.tudelft.nl) 是代尔夫特理工大学的计算机科学研究员。他自 1982 年以来一直使用 Unix(自 0.99.13 以来使用 Linux),并且是一个深刻的 MS 仇恨者(他们所有的产品都是坏事)。他喜欢非虚拟的比利时啤酒,并且是一个真正的环球旅行家(已经环游世界两次)。