本节书摘来异步社区《Java编码指南:编写安全可靠程序的75条建议》一书中的第1章,第1.8节,作者:【美】Fred Long(弗雷德•朗), Dhruv Mohindra(德鲁•莫欣达), Robert C.Seacord(罗伯特 C.西科德), Dean F.Sutherland(迪恩 F.萨瑟兰), David Svoboda(大卫•斯沃博达),更多章节内容可以访问云栖社区“异步社区”公众号查看。
指南8:防止XPath注入
可扩展标记语言(Extensible Markup Language,XML)可用于以类似于关系数据库的方式来存储数据。XML文档中的数据通常是用XPath来检索。当给XPath检索例程提供的数据没有做合适的无害化处理时,就有可能会导致XPath注入(XPath injection)攻击。这种攻击类似于SQL注入或XML注入(参见《The CERT® Oracle® Secure Coding Standard for Java™》[Long 2012]的“IDS00-J. Sanitize untrusted data passed across a trust boundary”。攻击者可以在查询用的数据字段中输入有效的SQL构造或XML构造。典型的攻击是,让条件查询字段解析为一个永真式,这样就会导致攻击者访问到未经授权的信息。
该指南是指南7的一个具体示例。
XML路径注入示例
先来看看下面的XML模式。
<users>
<user>
<username>Utah</username>
<password>e90205372a3b89e2</password>
</user>
<user>
<username>Bohdi</username>
<password>6c16b22029df4ec6</password>
</user>
<user>
<username>Busey</username>
<password>ad39b3c2a4dabc98</password>
</user>
</users>```
密码已被散列加密,这符合指南13。出于演示目的,这里的密码就用MD5散列算法加密;在实践中,应该使用SHA-256这样的更安全的算法。
不可信的代码可能会尝试在用户输入中动态构造XPath语句,然后利用这个语句从XML文件中检索出用户的详细信息。
//users/user[username/text()='&LOGIN&' and
password/text()='&PASSWORD&' ]`
如果攻击者知道Utah是一个有效的用户名,他可以指定一个下面这样的输入:
Utah' or '1'='1````
这样就构造出以下查询字符串:
//users/user[username/text()='Utah' or '1'='1'
and password/text()='xxxx']`
因为'1'='1'自动为真,所以密码永远也不会被检查。因此,攻击者在不知道用户Utah的密码的情况下被不适当地验证成了该用户。
违规代码示例
下面的违规代码示例从用户输入中读取用户名和密码,并使用它们来构建查询字符串,将密码以字符数组的形式传递,然后对其进行散列加密。这个示例容易受到上面提到的那种方式的攻击。如果将上面描述的攻击字符串传递给evaluate()方法,这个方法调用会返回XML文件中的相应节点,这会导致doLogin()方法返回true,并绕过所有授权。
private boolean doLogin(String userName, char[] password)
throws ParserConfigurationException, SAXException,
IOException, XPathExpressionException {
DocumentBuilderFactory domFactory =
DocumentBuilderFactory.newInstance();
domFactory.setNamespaceAware(true);
DocumentBuilder builder = domFactory.newDocumentBuilder();
Document doc = builder.parse("users.xml");
String pwd = hashPassword( password);
XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();
XPathExpression expr =
xpath.compile("//users/user[username/text()='" +
userName + "' and password/text()='" + pwd + "' ]");
Object result = expr.evaluate(doc, XPathConstants.NODESET);
NodeList nodes = (NodeList) result;
// Print first names to the console
for (int i = 0; i < nodes.getLength(); i++) {
Node node =
nodes.item(i).getChildNodes().item(1).
getChildNodes().item(0);
System.out.println(
"Authenticated: " + node.getNodeValue()
);
}
return (nodes.getLength() >= 1);
}```
合规解决方案(XQuery)
为了防止XPath注入,可以采用类似于防止SQL注入的方式。
将所有的用户输入视为不可信,并执行适当的无害化处理。
对用户输入进行无害化处理时,验证数据类型、数据长度、数据格式和数据内容的正确性。例如,使用一个正则表达式检查用户输入中是否包含XML标签和特殊字符。这种做法符合对用户输入进行无害化处理的规范。更多细节参见指南7。
在客户端-服务器(client-server,CS)应用程序中,既执行客户端验证,也执行服务器端验证。
广泛地测试用于提供、传播或接受用户输入的应用程序。
一种有效防止SQL注入相关问题的技术是参数化。参数化能确保将用户指定的数据以参数的形式传递给API,这样数据就不会被解释为可执行内容了。遗憾的是,Java SE目前缺乏一个类似于XPath查询的接口。不过,通过使用XQuery这样的接口,XPath可以模拟SQL参数化。XQuery支持将查询语句写入运行时环境中的一个单独文件中。
输入文件:login.xq
declare variable $userName as xs:string external;
declare variable $password as xs:string external;
//users/user[@userName=$userName and @password=$password]`
下面的合规解决方案从一个文本文件中读取所需的特定格式的查询语句,然后将用户名和密码的值插入一个映射中。XQuery库构造了这些来自用户输入的XML查询。
private boolean doLogin(String userName, String pwd)
throws ParserConfigurationException, SAXException,
IOException, XPathExpressionException {
DocumentBuilderFactory domFactory =
DocumentBuilderFactory.newInstance();
domFactory.setNamespaceAware(true);
DocumentBuilder builder = domFactory.newDocumentBuilder();
Document doc = builder.parse("users.xml");
XQuery xquery =
new XQueryFactory().createXQuery(new File("login.xq"));
Map queryVars = new HashMap();
queryVars.put("userName", userName);
queryVars.put("password", pwd);
NodeList nodes =
xquery.execute(doc, null, queryVars).toNodes();
// Print first names to the console
for (int i = 0; i < nodes.getLength(); i++) {
Node node =
nodes.item(i).getChildNodes().item(1).
getChildNodes().item(0);
System.out.println(node.getNodeValue());
}
return (nodes.getLength() >= 1);
}```
使用这种方法,用户名(userName)和密码(password)字段中输入的数据不会被运行时环境解释为可执行的内容。
适用性
未能验证用户输入可能会导致信息披露和未经授权代码的执行。
根据OWASP [OWASP 2013]:
(防止XPath注入)需要被删除(即禁止)或适当转义的字符如下。
``< > / ' = "``可用于防止直接的参数注入。
XPath查询不应该包含任何元字符(如'、=、*、?、//或类似字符)。