规则1.1 禁止直接使用外部输入来拼接SQL语句以防止SQL注入
说明:如果对于外部输入的数据未经处理,直接用于拼接SQL语句,攻击者可以通过构造特殊形式的输入来改变程序中原本要执行的SQL逻辑,形成SQL注入攻击,导致系统功能异常、信息泄露、数据被非法修改等安全问题。
错误示例:
publicvoid doPrivilegedAction(String username, char[] password) throws SQLException
{
/* … */
try
{
String pwd =hashPassword(password);
String sqlString = "SELECT * FROM db_user WHEREusername = '"
+ username + "' AND password = '" + pwd + "'";
Statement stmt =connection.createStatement();
ResultSet rs= stmt.executeQuery(sqlString);
// Authenticate …
}
/* … */
}
上面的代码使用用户输入来拼接SQL语句,但又没有对用户的输入做校验,存在SQL注入风险。当用户为username输入值:jack' OR '1' = '1
则SQL语句变成了:SELECT * FROM db_user WHERE username='jack' OR '1'='1' AND
password=<PASSWORD>
如果jack是有效用户,这样便绕开了对口令的验证。
推荐做法1:校验输入长度,并且使用PreparedStatement来防范SQL注入。
publicvoid doPrivilegedAction(String username, char[] password)
throws SQLException
{
/* … */
try
{
String pwd =hashPassword(password);
// Ensure that the length of user name is legitimate
if ((username.length()) > 8)
{
// Handle error
}
String sqlString = "select * from db_user whereusername=? and password=?";
PreparedStatement stmt =connection.prepareStatement(sqlString);
stmt.setString(1, username);
stmt.setString(2, pwd);
ResultSet rs= stmt.executeQuery();
// Authenticate …
}
/* … */
}
使用PreparedStatement,输入的参数无法改变原始的SQL语义。上例中,即使攻击者插入类似 jack’ or ‘1’=‘1的字符串,也只会将此字符串当做username来查询。
推荐做法2:使用存储过程
publicvoid doPrivilegedAction(String username, char[] password)
throws SQLException
{
/* … */
try
{
String pwd =hashPassword(password);
// Ensure that the length of user name is legitimate
if ((username.length()) > 8)
{
// Handle error
}
String sqlString = "select * from db_user whereusername=? and password=?";
CallableStatement cs = connection.prepareCall("{call sp_getUser(?,?)}");
cs.setString(1, username);
cs.setString(2, pwd);
ResultSet rs= cs.executeQuery();
// Authenticate …
}
/* … */
}
使用存储过程的效果和使用PreparedStatement类似,其区别是存储过程需要先将SQL语句定义在数据库中。但需要注意的是,存储过程中也可能存在注入问题,因此应该尽量避免在存储过程内使用动态的SQL语句。
推荐做法3:对外部输入进行转义
publicvoid doPrivilegedAction(String username, char[] password)
throws SQLException
{
/* … */
try
{
String pwd =hashPassword(password);
Codec oe = new OracleEncoder();
String susername =oe.encode(username);
String spwd = oe.encode(pwd);
String sqlString = "SELECT * FROM db_user WHERE username= '"
+ susername + "' AND password = '" + spwd + "'";
Statement stmt =connection.createStatement();
ResultSet rs= stmt.executeQuery(sqlString);
// Authenticate …
}
/* … */
}
对于PreparedStatement无法适用或者存储过程内部也存在动态构造SQL语句的情形,则可以考虑对外部输入做转义。每个DBMS都有一个字符转义机制来告知DBMS输入的是数据而不是代码,如果将所有用户的输入都进行转义,那么DBMS就不会混淆数据和代码,也就不会出现SQL注入了。针对每种数据的转义机制实现,可以使用现有的API工具,比如OWASP ESAPI的escaping routines,也可以使用自己的实现。
规则1.2 禁止直接使用外部输入来拼接XML数据以防止XML注入
说明:构造XML节点时,当XML中包含有未经审查的用户输入时,可能会产生XML注入攻击。恶意攻击者伪造XML数据,改变XML的原有结构,达到攻击的目的。
错误示例:
privatevoid createXMLStream(BufferedOutputStream outStream, Stringquantity) throws IOException
{
StringxmlString;
xmlString = "<item>\n<description>Widget</description>\n"
+ "<price>500.0</price>\n" + "<quantity>" + quantity
+ "</quantity></item>";
outStream.write(xmlString.getBytes());
outStream.flush();
}
对这段示例代码,恶意用户可能为quantity输入:
"1</quantity><price>1.0</price><quantity>1"
从而XML 变成如下片段,XMP解析器可能取出的price为1,而不是500.0
<description>Widget</description>
<price>500.0</price>
<quantity>1</quantity><price>1.0</price><quantity>1</quantity>
</item>
推荐做法:使用正则表达式对构成XML节点的数据进行校验。
privatevoid createXMLStream(BufferedOutputStream outStream, Stringquantity) throws IOException
{
// Write XML string if quantity contains numbers only.
// Blacklisting of invalid characters can be performed
// in conjunction.
if (!Pattern.matches("[0-9]+", quantity))
{
// Format violation
}
String xmlString= "<item>\n<description>Widget</description>\n"
+ "<price>500</price>\n" + "<quantity>" + quantity
+ "</quantity></item>";
outStream.write(xmlString.getBytes());
outStream.flush();
}
构造XML节点的方法有很多,以上方法是直接构造的,需要做这种校验。其他用Schema校验,只要保证输出符合原来的XML Schema定义,没有歧义,没有风险即可。
由于对数据进行检验或过滤很难列举全所有的非法数据,建议采用以下两种方式:
1、使用对象的形式来替代直接拼装的XML;
2、对用户输入的数据使用<![CDATA[…]]>封装。
规则1.3 对字符串校验之前先要对其做归一化
说明:对外部输入字符串校验之前,需要使用java.text.Normalizer的normalize()方法先对其进行归一化(Unicode Normalization)处理。归一化可以确保具有相同意义的字符串具有统一的二进制描述。推荐使用的归一化格式是NFKC,因为它将输入转换成可与目标进行安全比较的等效标准格式。
错误示例:
// String s may be user controllable
String s = "\uFE64" + "script" + "\uFE65";
// Validate
Pattern pattern = Pattern.compile("[<>]"); // Check for angle brackets
Matcher matcher = pattern.matcher(s);
if (matcher.find())
{
// Found black listed tag
thrownew IllegalStateException();
}
// . . .
如上示例代码的本意是防止外部输入中包含正反尖括号: < (\u003C)和 > (\u003E)。但是在Unicode中正反尖括号还有另外一种非标准的表达方式:< (\uFE64)和> (\uFE65)。攻击者可以利用这种非标准的表达方式来绕过输入检查。
推荐做法:
String s = "\uFE64" + "script" + "\uFE65";
// Normalize
// \uFE64 is normalized to < and \uFE65 is normalized to> using NFKC
s = Normalizer.normalize(s,Form.NFKC);
// Validate
Pattern pattern = Pattern.compile("[<>]");
Matcher matcher = pattern.matcher(s);
if (matcher.find())
{
// Found black listed tag
thrownew IllegalStateException();
}
// . . .
在进行校验之前先对输入的字符串做归一化处理,可以将使用非标准表达方式的字符串转换成统一的标准表达形式,从而使其不能绕过输入校验。
规则1.4 禁止直接使用用户输入的数据来构造格式化字符串
说明:Java解释格式化字符串比较严格,当匹配对应的格式化字符,转换参数失败时,标准库会抛出异常,这种方式可以减少被恶意攻击的机会。但是恶意用户仍然可以通过格式化字符串注入引起信息泄露或者拒绝服务攻击。因此,不能直接将来自不可信源的字符串用于构造格式化字符串。
错误示例:
class Format
{
static Calendar c = new GregorianCalendar(1995, GregorianCalendar.MAY, 23);
publicstaticvoid main(String[] args)
{
// args[0] is the credit card expiration date
// args[0] can contain either %1$tm, %1$te or %1$tY as malicious arguments
// First argument prints 05 (May), second prints 23 (day)
// and third prints 1995 (year)
// Perform comparison with c, if it doesn't match print the following line
System.out.printf(args[0]
+ " did not match! HINT: It wasissued on %1$terd of some month",
c);
}
}
上面代码中,如果arg[0]包含恶意格式化字符串%1$tm,%1$te或者%1$tY,则会导致用来做比较验证的日期信息被暴露。
推荐做法:将用户的输入排除在格式化字符串之外
class Format
{
static Calendar c = new GregorianCalendar(1995, GregorianCalendar.MAY, 23);
publicstaticvoid main(String[] args)
{
// args[0] is the credit card expiration date
// Perform comparison with c,
// if it doesn't match print the following line
System.out.printf("%s did notmatch! "
+ " HINT: It was issued on %1$terdof some month", args[0], c);
}
}