编写CGI过程中一个值得注意的安全问题

---摘自《酷比网论坛》
本人利用perl编写了一个论坛CGI程序,名字叫"小兵中文论坛",并用这个CGI程序在自己的主页上构筑了一个论坛。程序全部采用结构性文本文件保存资料,如用户名字密码资料、帖子内容文件、帖子目录文件(文件名m ain.txt,存放帖子编号、标题、发言者名字、发表日期、回复数及点击数等资料,用于生成论坛界面中的帖子点击链接),程序一直运行良好,但是在5 月11日,却发现论坛界面中5月10日前用户所发的帖子全部不见了,这令我大吃一惊,直觉告诉我这肯定是有人捣蛋,于是马上FTP到我的主页,一看帖子文件都完好无缺,但是m ain.txt的字节数却不对头,只剩下几百字节了,马上将main.txt拉了回来了,打开一看,发现了问题所在:约在5月10日零时,一个别有用心的人(注册名为h acker)用一个特殊的符号作为帖子标题发了一个帖子,致使main.txt文件中5月10日前的数据被删除了。这令我对这个符号产生了极大的兴趣,于是在恢复了m ain.txt的数据后(论坛程序留有一手,可以根据帖子文件恢复main.txt数据^_^),对这个符号研究了一翻,发觉它对用perl编写的、用文本文件存放用户发送的资料的C GI程序(如很多论坛或聊天室)有很大的危害性,造成数据丢失及程序运行异常。为了让大家在编写CGI时注意到这个问题,本人特意写了这篇文章,算是抛砖引玉。
 各位,看到这里也许你们会说:这太可笑了吧,一个符号就把你的论坛黑了?这真让我感到惭愧,不过你可千万别小看这个符号,说它特殊,是因为它是一个可见的、可复制粘贴的、p erl把它当成文件结束符的一个字符。实话说刚开始我也把问题想得很简单:在程序里加入几行命令,让程序对这个特殊字符进行过滤,删除这个特殊字符不就行了?可是,我马上发现这根本行不通。要这样做必须将这个字符放在程序中,这样程序才能对用户输入的资料进行匹配以便鉴定这些资料是否存在这个字符,但是由于这个字符是一个文件结束符,使得程序总是运行出错(程序运行前,系统首先要读取程序文件,但是读取过程却在程序文件中的这个特殊字符处结束了,所以程序文件没有完整地被读取,程序运行当然会出错,即使在这个字符前加上反斜扛\ 或#作为注释也不行)。看来此路不通,接着我试图查获该字符的ASCII码,也失败了,看来这算是perl的一个BUG。现在我先回头说一下这个字符是如何造成m ain.txt文件中数据丢失的,为了便于后面的说明,我先对main.txt的结构作点说明及程序中处理main.txt的部份简化后写出来:
main.txt的结构是一个帖子记录占用两行,如下:
D-100-这是一个示范的帖子-过河卒-2000/05/15 12:02:01(2)[32]
< !--end: 100-->


其中第一行内容顺序是:帖子类型、编号、标题、发言者姓名、时间、回复数、点击数,第二行是表示帖子资料结束。每新增一个帖子,新帖子的资料插入文件头部。论坛程序运行中先读取这个文件后根据帖子记录生成论坛界面H TML文件。
程序添加新增帖子资料到main.txt的部份如下:
open (MAIN,"main.txt"); # 以读取方式打开main.txt
@main = ; # 将main.txt中的内容赋与数组@main
close(MAIN); # 关闭main.txt
open (MAIN,">main.txt"; # 以写入方式打开main.txt
print MAIN @post; # 写入用户发送的资料@post
print MAIN @main; # 写入main.txt原来的数据
close(MAIN); # 关闭main.txt
当用这个特殊字符作为帖子标题发了一个帖子后(假设帖子编号为100),main.txt中就有了这个符号(在编号100帖子的数据中),此时m ain.txt还保持完整,即数据还没有发生丢失。但是,如果接着别的用户又发了一个帖子(帖子编号为101),则程序要先读取main.txt(命令行:o pen (MAIN,"main.txt"); @main = ; close(MAIN); ),接着以写入方式再打开main.txt(命令行:open (MAIN,">main.txt";),在main.txt文件的头部添加编号101帖子的资料后(命令行:print MAIN @post; ),还要将main.txt文件原来的数据写入到这个帖子资料的后面(命令行:print MAIN @main;),由于编号100的帖子完成发送之后,main.txt已经含有这个特殊的字符,程序在读取main.txt时,并没有完整地读取所有的数据,正如前面所说的,因为程序把这个字符当甩了文件结束符,所以程序仅仅读取了这个符号之前的数据,造成数组@ main只含有编号100的帖子的部份资料,而不是原来main.txt完整的数据,因此在print MAIN @main;这一步骤中,main.txt被破坏,只写入了101帖子的资料及帖子100的部份资料,之前的帖子资料全部丢失。
如果把程序改为每新增一个帖子,新帖子的资料插入main.txt尾部,则这个字符虽然不会造成main.txt被破坏,但是之后新添加的帖子无法显示。
为了进一步证实这个字符的危害性,我用这个字符对国外一个很著名的论坛程序Ultimate Bulletin Board(简称UBB,perl编写,国内有很多论坛是使用这个程序的汉化版)的5.45版本进行了测试。UBB的用户名字和密码放在文件members list.cgi中,实际上它是一个文本文件,最每新增一个用户,用户资料添加到文件尾部。测试中发现,如果某一个用户注册时名字含有这个字符的话,以后的注册用户都不能发言,因为程序在读取用户资料文件时在这个字符处结束了,后面的用户没有被理会,所以程序会说" 你不是注册用户"。同样地,对其它几个论坛CGI及聊天室CGI也作了测试,由于它们都是用文本文件保存用户资料,其中有两个论坛CGI也是采用与本人的" 小兵中文论坛"类似的做法将帖子的一些资料放入一个文本文件中,而不是直接放入一个HTML文件中,所以,测试结果在我的预料之中:这个符号都导致它们出现与U BB相同的问题,而哪两个论坛出现在与本人的论坛同样的问题。
  这个字符真是太利害了,不过后来我想出了一个招数来对付它,这一招就是"以其人之道还其人之身",即是利用它是一个文件结束符这一特性反过来对付它。具体做法是以一个临时文件作为过渡,先将用户输入的资料写入这个临时文件,然后再从这个临时文件读取出这些资料,因为这个字符是一个结束符,所以如果用户输入的资料中含有这个字符的话,则从临时文件重新读取资料的过程在这个字符处结束,读取出来的资料并不包含这个字符,这样就达到了过滤掉这个特殊字符的目的。下面是示例源码,对帖子的标题( $subject)和发言者名字($name)进行处理以过滤这个特殊字符:

……用户输入资料后,程序分别将帖子标题和用户名字赋与变量$subject、$name……

$retval = rand(1000000); #生成一个随机浮点数
$retval = int($retval); #舍去小数部份
open (TMP,">$retval.tmp");
print TMP "$subject\n";
print TMP "$name";
close(TMP); #写入临时文件结束
open (TMP,"$retval.tmp");
@tmp = ;
close(TMP);
$subject = $tmp[0]; #如果原$subject包含特殊符号,则此时已经过滤掉了
$name = $tmp[1]; #如果原$name包含特殊符号,则此时已经过滤掉了
$subject =~ s/\r|\n//g;
if ($subject eq ""){
&Head("错误","没有填写标题或标题中含有非法字符!");
exit;
}
$name =~ s/\n//g;
if ($name eq ""){
&Head("错误","没有填写名字或名字中含有非法字符!");
exit;
}
unlink("$retval.tmp"); #删除临时文件

另外,其它编写CGI的语言如C、php等不知是否存在上述的这个问题,本人没有进行测试。
这件事给我们的教训是:不要想当然地以为用户会按照自己的意愿输入资料,往往安全隐患就因此埋下了。我在编写这个论坛CGI的时候,也考虑到了用户输入各种特殊字符的可能性,但是由于在此之前我并不知道有这么一个字符,致使被别人钻了空子,不过幸亏本人在编写程序过程中考虑到了m ain.txt万一被破坏后如何恢复的问题(做什么事都要留一手啊),才不致于造成损失。