避免 XPath 注入的危险

原文链接:点击访问

意识到风险以便更好地保护 XML 应用程序
级别: 中级
Robi Sen ([email protected]), 服务副总裁, Department13
2007 年 9 月 17 日

随着简单 XML API、Web 服务和 Rich Internet Applications (RIAs) 的发展,更多组织几乎在所有方面(从配置文件到远程过程调用)都采用 XML 作为数据格式。一些人已经使用 XML 文档代替更传统的纯文本文件或关系数据库,但是与任何其他允许外部用户提交数据的应用程序或技术相似,XML 应用程序可能容易受到代码注入的攻击,尤其是 XPath 注入攻击。

简介

随着新技术的出现并得到很好的沿用,针对这些技术的威胁也随之产生并逐渐增多。SQL 盲注攻击是一种为人熟知的代码注入攻击形式,但是也有很多其他形式,有些尚未得到很好的记载和了解。最近开始出现的一种代码注入攻击是 XPath 注入攻击,它利用了 XPath 解析器的松散输入和容错特性,让心怀不满的人能够在 URL、表单或其他方法上附带恶意的 XPath 查询以获得权限信息的访问权并更改这些信息。

本文考察了通常情况下如何执行 XPath 攻击并提供了一个 Java? 和 XML 环境中的例子。讨论了如何检测这类威胁,考察了如何减轻该威胁,最后讨论了如何应对可疑的入侵。

入门

本文主要介绍代码注入攻击的一种特殊类型:XPath 盲注。如果您不熟悉 XPath 1.0 或需要了解基础知识,请查看 W3 Schools XPath 教程(请参阅 参考资料 中的链接)。您还可以在 developerWorks 上找到大量的关于在各种语言环境中使用 XPath 的文章(请参阅 参考资料 中的链接)。本文使用的示例主要针对 XPath 1.0,但是也可用于 XPath 2.0。XPath 2.0 实际上增加了您可能遇到的问题。

本文还提供了用于处理 Java JDK 5.0 的 Java 代码示例。同时本文的概念和主题是跨平台的,如果您的应用程序使用 XPath 获取特殊的代码示例,那么您必须使用 JDK 5.0。

代码注入

一种更常见的对 Web 应用程序的攻击和威胁是某种形式的代码注入,Wikipedia 将其定义为:

……利用系统没有对其输入进行强制执行或检查的假设向计算机系统中引入(或 “注入”)代码的技术。注入代码的目的通常是绕过或修改程序的最初目标功能。如果被绕过的功能涉及系统安全,那么结果可能是灾难性的。

快速浏览任何相关 Web 站点(比如 Web Application Security Consortium 或 Security Focus,请参阅 参考资料 中的链接)都会显示很多使用某种形式的代码注入进行的攻击,从 JavaScript 到 SQL 注入再到其他形式的代码注入攻击。最近开始出现的一种威胁(最初由 Amit Klein 于 2004 年在一篇论文中概述)是 XPath 盲注攻击(请参阅 参考资料)。这种攻击的运作跟 SQL 盲注攻击几乎完全相似,与 SQL 注入攻击不同的是,几乎没什么人了解 XPath 盲注攻击或对其进行预防。与 SQL 注入攻击类似,如果使用最佳实践开发安全的应用程序,通常可以轻松地处理该威胁。

XPath 攻击

一般说来,大多数 Web 应用程序使用关系数据库存储和检索信息。例如,如果您的 Web 站点需要身份验证,那么您可能拥有一个 users 表,其中包含惟一 ID、登录名、密码,也许还有一些其他信息,比如角色。从 users 表中检索用户的 SQL 查询可能类似于清单 1。

清单 1. 从 users 表中检索用户的 SQL 查询Select * from users where loginID='foo' and password='bar' 在这个查询中,用户必须提供 loginID 和 password 作为输入。如果攻击者在 loginID 字段中输入:' or 1=1 并在 password 中输入:' or 1=1,则形成的查询将类似清单 2。

清单 2. 从攻击者输入形成的查询Select * from users where loginID = '' or 1=1 and password=' ' or 1=1这个条件会一直匹配,因此攻击者可以进入系统。XPath 注入的原理大体类似。但是,假设您拥有的不是一个 users 表,而是一个 XML 文件,其中包含了如清单 3 所示的用户信息。

清单 3. user.xml<?xml version="1.0" encoding="UTF-8"?>
<users>
<user>
<firstname>Ben</firstname>
<lastname>Elmore</lastname>
<loginID>abc</loginID>
<password>test123</password>
</user>
<user>
<firstname>Shlomy</firstname>
<lastname>Gantz</lastname>
<loginID>xyz</loginID>
<password>123test</password>
</user>
<user>
<firstname>Jeghis</firstname>
<lastname>Katz</lastname>
<loginID>mrj</loginID>
<password>jk2468</password>
</user>
<user>
<firstname>Darien</firstname>
<lastname>Heap</lastname>
<loginID>drano</loginID>
<password>2mne8s</password>
</user>
</users>
在 XPath 中,类似于 SQL 查询的语句如清单 4 所示。

清单 4. 匹配 SQL 查询的 XPath 语句//users/user[loginID/text()='abc' and password/text()='test123']要执行类似的攻击以绕过身份验证,您可能会使用类似清单 5 的方法。

清单 5. 绕过身份验证//users/user[LoginID/text()='' or 1=1 and password/text()='' or 1=1]您可能在 Java 应用程序中有一个诸如 doLogin 之类的方法,使用 清单 3 中的 XML 文档再次执行身份验证。可能类似于清单 6。

清单 6. XPathInjection.javaimport java.io.IOException;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
import javax.xml.parsers.*;
import javax.xml.xpath.*;

public class XpathInjectionExample {

public boolean doLogin(String loginID, String password)
throws ParserConfigurationException, SAXException,IOException,
XPathExpressionException {

DocumentBuilderFactory domFactory = DocumentBuilderFactory.newInstance();
domFactory.setNamespaceAware(true);
DocumentBuilder builder = domFactory.newDocumentBuilder();
Document doc = builder.parse("users.xml");

XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();
XPathExpression expr = xpath.compile("//users/user[loginID/text()='"+loginID+"'
and password/text()='"+password+"' ]/firstname/text()");
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++) {
System.out.println(nodes.item(i).getNodeValue());}

if (nodes.getLength() >= 1) {
return true;}
else
{return false;}
}
}
对于 清单 6,如果您传入一个 login 和 password,比如 loginID = 'abc' 和 password = 'test123',则该类将返回 true(而且就本例而言,还会打印一列 first name 到控制台)。例如,如果您传入类似 ' or 1=1 or ''=' 的值,那么您也会得到 true 返回值,因为 XPath 会最终发现字符串变成了类似清单 7 的样子。

清单 7. 字符串//users/user[loginID/text()='' or 1=1 or ''='' and password/text()='' or 1=1 or ''='']这个字符串会在逻辑上使查询一直返回 true 并将一直允许攻击者访问系统。

另一种可能性更大而且可能更加麻烦的 XPath 攻击方式是,攻击者可以利用 XPath 在应用程序中动态地操作 XML 文档。

提取 XML 文档结构

用于绕过身份验证的查询还可以用于提取 XML 文档的信息。假设攻击者猜测 XML 文档中第一个子节点的名称是 loginID 并且想要确认一下。攻击者可以提供清单 8 所示的输入。

清单 8. 攻击者提供的输入abc' or name(//users/LoginID[1]) = 'LoginID' or 'a'='b 与 清单 7 中的 1=1 不同,清单 8 中给出的表达式检查第一个子节点的名称是否为 loginID。形成的查询如清单 9 所示。

清单 9. 查询String(//users[LoginID/text()='abc' or name(//users/LoginID[1]) =
'LoginID' or 'a=b' and password/text()=''])
用试凑法,攻击者可以检查 XML 文档的各个子节点并通过查看这个 XPath 表达式是否能促成成功的身份验证以收集信息。攻击者然后可能编写出一个简单的脚本,发送各种 XPath 注入并从系统中提取 XML 文档,如 Klein 的论文中提及的那样。

XPath 注入预防

因为 XPath 注入攻击与 SQL 注入攻击非常类似,所以很多预防方法也类似。这些预防方法中,多数也可以类似地应用于预防其他类型的代码注入攻击。

身份验证

不论面对何种应用程序、环境或语言,都应遵守以下的最佳实践:

  • 假定所有输入都可疑。
  • 不仅要验证数据的类型,还要验证其格式、长度、范围和内容(例如,一个简单的正则表达式 if (/^"*^';&<>()/) 就可以找出大多数可疑的特殊字符)。
  • 在客户机和服务器上都要验证数据,因为客户机验证非常容易绕过。
  • 根据安全软件开发的最佳实践,遵守一致的编写和 [missing word] 策略实现应用程序安全性(请参阅 参考资料 中 Apache 的优秀 Web 服务列表)。
  • 在发布应用程序之前测试已知的威胁。参考资料 中的 “Fuzz Testing” 介绍了测试方法。

参数化

与多数数据库应用程序不同,XPath 不支持参数化查询的概念,但是您可以使用其他 API(比如 XQuery)模拟该概念。您不需要构建字符串表达式,然后传给 XPath 解析器以便在运行时动态执行(如清单 10 所示),而是通过创建保存查询的外部文件使查询参数化(如 清单 11 所示)。

清单 10. 传给 XPath 解析器的字符串"//users/user[LoginID/text()=' " + loginID+ " ' and password/text()='
"+ password +" ']"
在清单 11 中,通过创建保存查询的外部文件使查询参数化。

清单 11. dologin.xqdeclare variable $loginID as xs:string external;
declare variable $password as xs:string external;//users/user[@loginID=
$loginID and @password=$password]
对 清单 11 稍加修改,也可以完成同样的功能,如清单 12 所示。

清单 12. XQuery 片段Document doc = new Builder().build("users.xml");
XQuery xquery = new XQueryFactory().createXQuery(new File("
dologin.xq"));
Map vars = new HashMap();
vars.put("loginid", "abc");
vars.put("password", "test123");
Nodes results = xquery.execute(doc, null, vars).toNodes();
for (int i=0; i < results.size(); i++) {
System.out.println(results.get(i).toXML());
}
这样可以防止重要的显式变量 $loginID 和 $password 在运行时被作为可执行表达式处理。这样就分开了执行逻辑与数据;遗憾的是,查询参数化并不是 XPath 功能的一部分,但是可以从 SAXON 之类的开源解析器中免费获取(请参阅 参考资料 中的链接)。某些其他解析器支持这种功能,而且它可以作为防止 XPath 注入的一种可靠方式。

Web 服务器上的数据检查

要防止 XPath 注入和其他形式的代码注入,应该检查所有从 Web 服务器传到后端服务的数据。例如,对于 Apache 您可以使用 Mod_Security 筛选器(比如 SecFilterSelective THE_REQUEST "(\'|\")")查找字符串中的单引号和双引号并禁止它们。您也可以使用同样的方法筛选和禁止其他形式的特殊字符,比如 ("*^';&><</),这些字符都可以用于各种注入攻击。这种方法对于使用基于 REST 或 SOAP 的 XML 服务的应用程序可能很好,但是在其他情况下可能不适用。通常的最佳实践是,从最初的设计到应用程序实现都采用智能安全设计。

如果系统被破坏了怎么办?

大多数组织都考虑到了威胁检测和威胁拒绝,但很少为此作出规划,对于一名合格的安全专家,如果系统遭到破坏怎么办。您应该总是假定最坏的场景并作出规划。

这在很大程度上取决于组织和遭遇入侵的系统类型,但通常的最佳方法是,让系统脱机并等待专业的论证工程师检查系统。有时人们直接让系统脱机并重映像驱动器,但是这种做法消除了入侵的证据和入侵者可能给系统造成的其他损害的信息。如果可能的话,一直保留系统状态以待安全专家检查。

结束语

多数使用 XML 的应用程序不易受到 XPath 注入攻击,不应该只是因为发现了一个特殊的漏洞就认为 XML 应用程序风险更大。同时,随着越来越多的新平台的采用,比如 Ajax 和 FLEX 或 Open Laszlo 之类的 RIA 平台,以及来自 Google 等组织的 XML 服务联盟 —— 从与后端服务通信到持久性的几乎所有应用严重依赖于使用 XML,开发人员需要意识到这些方法所带来的威胁和风险。

遗憾的是,解决问题的方法和原则总是落后于新的特殊威胁的出现。遵守良好的安全最佳实践不仅能够帮助您预防 XPath 注入攻击,而且可以预防其他形式的攻击。

参考资料

学习

关于作者

Robi Sen 是 IT 顾问公司 Department 13 LLC 的服务副总裁。他在大部分时间里帮助客户(从财富 500 强公司到开始创业的小企业)定义和管理其技术挑战。他编写过很多方面的各种技术文章,而且经常出席各种大会并发表演讲。

相关日志

发表评论