用Perl编写CGI过程中的一个安全隐患

Author: 过河卒 Date: 2000年 第28期

  本人利用Perl编写了一个论坛CGI程序,并用这个CGI程序在自己的主页上构筑了一个论坛。程序全部采用结构性文本文件保存资料,如用户名、密码、帖子内容文件、帖子目录文件(文件名main.txt,存放帖子编号、标题、发言者名字、发表日期、回复数及点击数等资料,用于生成论坛界面中的帖子点击链接)。程序一直运行良好,但是在5月11日,却发现论坛界面中5月10日前用户所发的帖子全部不见了,这令我大吃一惊。直觉告诉我这肯定是有人捣蛋。于是马上FTP到我的主页,一看帖子文件都完好无缺,但是main.txt的字节数却不对头,只剩下几百字节了。我马上将main.txt拉了回来,打开一看,发现了问题所在:约在5月10日零时,一个别有用心的人(注册名为hacker)用一个特殊的符号作为帖子标题发了一个帖子,致使main.txt文件中5月10日前的数据被删除掉。这令我对这个符号产生了极大的兴趣,于是在恢复了main.txt的数据后,对这个符号研究了一番,发觉它对用Perl编写的、用文本文件存放用户发送的资料的CGI程序(如很多论坛或聊天室)有很大的危害性,造成数据丢失及程序运行异常。为了让大家在编写CGI时注意到这个问题,本人特意写了这篇文章,算是抛砖引玉,愿和大家共同探讨CGI安全方面的问题。
  各位,看了上面的内容也许你会说:这太可笑了吧,一个符号就把你的论坛黑了?这真让我感到惭愧,不过你可千万别小看这个符号,说它特殊,是因为它是一个可见的、可复制粘贴的、Perl把它当成文件结束符的一个字符。说实话刚开始我也把问题想得很简单:在程序里加入几行命令,让程序自动删除帖子标题中的这个特殊结束符不就行了?可是,我马上发现这根本行不通。要这样做必须将这个符号放在程序中,这样程序才能对用户输入的资料进行匹配以便鉴定这些资料是否存在这个字符。但是由于这个字符是一个文件结束符,使得程序总是运行出错(程序运行前,系统首先要读取程序文件,但是读取过程却在程序文件中的这个特殊字符处结束了,所以程序没有完整地被读取,程序运行当然会出错,即使在这个字符前加上反斜杠或#作为注释也不行),看来这算是Perl的一个BUG。现在我回头说一下这个字符是如何造成main.txt文件中数据丢失的。为了便于后面的说明,我先对main.txt的结构作点说明并将程序中处理main.txt的部分简化后写出来:
  main.txt的结构是一个帖子记录占用两行,如下:
  D-1001-这是一个示范的帖子-过河卒-2000/05/15 12:02:01(2)〖32〗
  <!--end: 100--></ul>
  其中第一行内容顺序是:帖子类型、编号、标题、发言者姓名、时间、回复数、点击数,第二行是表示帖子资料结束。每新增一个帖子,新帖子的资料插入文件头部。
  程序添加新增帖子资料到main.txt的部分如下:
  open (MAIN,″main.txt″);  # 以读取方式打开main.txt
      @main = <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帖子的资料中),此时main.txt还保持完整,即数据还没有发生丢失。但是,如果接着别的用户又发了一个帖子(帖子编号为101),则程序要先读取main.txt(命令行:open (MAIN,″main.txt″); @main = <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尾部,则这个字符虽然不会造成main.txt被破坏,但是之后新添加的帖子无法显示。
  为了进一步证实这个字符的危害性,我用这个字符对国外一个很著名的论坛程序Ultimate Bulletin Board(简称UBB,Perl编写,国内有不少论坛使用这个程序的汉化版)的5.37版本进行了测试。UBB用文本文件存放用户资料,新注册的用户资料放在文件尾部。测试中发现,如果某一个用户注册时名字含有这个字符的话,以后的注册用户都不能发言,因为程序在读取用户资料文件时便在这个字符处结束了,后面的用户没有被读入,所以程序会说"你不是注册用户"。同样地,对其它几个论坛和聊天室也作了测试,发现这个符号都导致它们出现与UBB相似的问题。
  这个字符真是太厉害了,不过后来我想出了一招来对付它,这一招就是“以其人之道还治其人之身”,具体是将用户所输入的资料先写入一个临时文件,然后再从这个临时文件读取出这些资料,既然这个字符是一个结束符,那么如果用户输入的资料中含有这个字符的话,则读取过程在这个字符处结束,读取出来的资料并不包含这个字符,下面是示例源码,对帖子的标题(|Subject)和发言者名字 (|Sname)进行鉴别:
      |Sretval = rand(1000000);              
  #生成一个随机浮点数
      |Sretval = int(|Sretval);               
  #舍去小数部分,得到一个整数
      open (TMP,″>|Sretval.tmp″);            
  #以这个整数为文件名建立一个临时文件
      print TMP ″|Ssubject\n)″;               
  #写入帖子标题
      print TMP ″|Sname″;                    
  #写入用户名字
      close(TMP);                           
  #写入临时文件结束,关闭它
      open (TMP,″|Sretval.tmp″);             
  #打开临时文件
      @tmp = <TMP>;                         
  #将临时文件的内容赋予数组@tmp
      close(TMP);
      |Ssubject =|Stmp[0];
      |Sname = |Stmp[1];
      |Ssubject =~ s/\n//g;
      if (|Ssubject eq ″″) {
         &Head(“错误”,“没有标题或标题中含有非法字符!”);
         exit;
      }
      |Sname =~ s/\n//g;
      if (|Sname eq ″″) {
         &Head(“错误”,“没有标题或标题中含有非法字符!”);
         exit;
      }
      unlink(″|Sretval.tmp″);              
  #删除临时文件
  也许这个办法不是最好的,如果哪位大虾有更好的办法,千万要指点小弟一下,我的E-mail是:qxy3@163.net。另外,其它编写CGI的语言如C、PHP等不知是否存在上述的这个问题,本人没有进行测试。