XPath注入的触发点通常出现在登录校验、用户查询、配置读取等需要从XML文档中筛选节点的地方。攻击者提交的数据一旦被直接拼进表达式,就可能改变XPath的求值条件,让原本的节点筛选逻辑失效。它和SQL注入的相似之处在于,都是把输入当成了可执行语法来处理。

一、产生原理:输入如何改变XPath结构
XML文档可以看作一棵节点树,XPath的作用是在这棵树上按路径定位节点。比如一个存储用户信息的文档,根节点下可能有多个user节点,每个user下又有username和password。最常见的登录校验会写类似 //user[username/text()='admin' and password/text()='123456'] 这样的表达式。
为了把用户提交的参数放入表达式,很多实现采用简单的字符串拼接。下面是一段存在漏洞的Java代码:
String username = request.getParameter("username");
String password = request.getParameter("password");
String xpath = "//user[username/text()='" + username + "' and password/text()='" + password + "']";
XPathFactory factory = XPathFactory.newInstance();
XPath xpathObj = factory.newXPath();
XPathExpression expr = xpathObj.compile(xpath);
NodeList nodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
如果用户正常输入 admin 和 123456,表达式逻辑没有问题。但当用户名传入 ' or '1'='1 时,最终表达式会变成类似下面这样的结构:
' or '1'='1
这个表达式的方括号内不再是两个必须同时成立的条件,而是变成了 username/text()='' or '1'='1' 这样的恒真条件。XPath的or运算会优先满足恒真分支,因此第一层过滤被绕过。攻击者甚至不需要知道任何有效用户名。
这说明XPath注入的核心不是某个特定函数的问题,而是字符串拼接导致用户输入可以闭合前面的单引号、补上新的谓词,再用后面的单引号保持语法完整。由于XPath表达式的语法比SQL更灵活,单引号、双引号、方括号、//、|、通配符等都可能被利用。
二、典型攻击:认证绕过与盲注提取
认证绕过是XPath注入最直接的利用方式。攻击者可以提交 ' or '1'='1,也可以使用更复杂的负载,例如 '] | //user | //*[text()='。这样不仅能绕过条件,还可能把整个文档中的user节点作为结果返回。由于XPath没有类似数据库的权限管理,一旦注入成功,攻击者通常能读取XML文档中任意节点。
盲注场景同样值得关注。很多应用不会直接回显查询结果,但会根据是否返回节点来决定登录成功、搜索命中或错误提示。攻击者可以通过布尔判断逐步还原文档结构。例如提交:
' or string-length(name(/*[1]))>3 or '
这段payload会判断根节点名称长度是否大于3。XPath 1.0中常用的函数包括count()、string-length()、name()、position(),这些函数可以构造非常细粒度的探测条件。比如先判断 //user 的数量,再逐层判断某个节点文本的字符。
相比SQL注入,XPath盲注有时更容易扩大影响,因为XML文档通常没有表、行、列这些边界,整个文档是一个可以被XPath路径描述的整体。攻击者可以从根节点一路枚举到底层,拿到完整数据。某些基于XPath 2.0实现还可能支持doc()函数读取外部文件,不过实际利用受限于库能力和网络环境。
三、防御措施:参数化查询与输入约束
最有效的防御方式是在执行XPath查询时使用变量绑定,而不是字符串拼接。参数化后,用户输入只作为变量值参与比较,不会破坏表达式结构。Java的XPath API提供了XPathVariableResolver,可以在编译表达式后注入变量:
final String username = request.getParameter("username");
final String password = request.getParameter("password");
XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();
xpath.setXPathVariableResolver(new XPathVariableResolver() {
@Override
public Object resolveVariable(QName varName) {
if ("username".equals(varName.getLocalPart())) {
return username;
}
if ("password".equals(varName.getLocalPart())) {
return password;
}
return null;
}
});
XPathExpression expr = xpath.compile("//user[username/text()=$username and password/text()=$password]");
NodeList nodes = (NodeList) expr.evaluate(doc, XPathConstants.NODESET);
变量使用 $username 和 $password 引用,传入的值只会和节点文本做相等比较。即使攻击者输入 ' or '1'='1,这个字符串也只会被当作普通文本,不再具有语法含义。Python的lxml库同样支持XPath变量,开发者应优先使用这类能力。
如果使用的XPath库不支持变量,至少要做严格的输入校验。对于用户名、编号这类字段,白名单是最可靠的方式:只允许字母、数字、下划线等安全字符,长度设上限。下面是一个简单的Java校验函数:
private static final Pattern SAFE_INPUT = Pattern.compile("[a-zA-Z0-9_]{1,32}");
public static String sanitize(String value) {
if (value == null || !SAFE_INPUT.matcher(value).matches()) {
throw new IllegalArgumentException("invalid input");
}
return value;
}
黑名单过滤单引号、双引号、方括号虽然能挡住一部分攻击,但容易被绕过。XPath语法中包含大量可用于注入的符号,例如 |、//、*、=、!=、<、> 等。正确的思路是不让输入进入语法层,而不是和攻击者比赛谁记得更多特殊符号。
此外还应该限制XPath查询能够读取的文档范围。避免把包含密码、密钥、内部配置的完整XML暴露给查询接口。对于公开的查询能力,可以预先解析XML并转为内存对象,再进行普通条件筛选。如果查询需求复杂,建议使用参数化模板和独立的数据访问层,集中处理过滤逻辑。
四、不同语言的安全实践与测试清单
Java代码中最常见的问题隐藏在XPathFactory编译之前。进行安全审计时,可以重点搜索String xpath = "..." + request.getParameter(...)这类模式。凡是用户输入直接通过加号进入compile()或evaluate()的路径,都应标记为高优先级风险。
Python开发中,标准库xml.etree.ElementTree虽然提供了findall()和xpath(),但它不支持变量绑定,而且对表达式中的参数处理比较原始。如果需要从外部参数拼装查询,建议改用lxml的XPath类,并通过变量传参。下面是一个参数化示例:
from lxml import etree
tree = etree.parse("users.xml")
username = request.args.get("username")
password = request.args.get("password")
result = tree.xpath(
"//user[username/text()=$u and password/text()=$p]",
u=username,
p=password,
)
该示例不会因为username参数包含单引号而改变查询结构。实际项目中,还应统一封装XML查询入口,禁止业务代码自行拼接XPath。对于不支持变量绑定的语言,可以使用查询模板加白名单参数位置的方式,把输入先经过正则校验再放入表达式。
测试阶段可以建立一组注入用例,包括 ' or '1'='1、'] | //*、' or count(//user)>5 or '、' or string-length(name(/*[1]))>3 or ' 等。通过观察登录是否被绕过、返回数据是否异常,能快速判断是否存在XPath注入。安全测试还应覆盖大小写混淆、URL编码、双重编码等输入形式,因为框架可能会在到达XPath引擎前解码一次或多次。
最后,XPath注入本质上是注入类漏洞的一种。修复时不要只盯着某一个payload,而要从数据与语法分离的角度重新设计查询链路。参数化查询、白名单校验、最小化查询范围这三项措施配合使用,可以显著降低XPath注入风险。

