XPath Injection¶
What does this mean ?¶
XPath injection occurs when untrusted data is inserted into the text of an XPath expression. A value intended to identify one XML record may instead change how the document is searched. XML entity escaping and XPath variable binding are different operations.
What can happen ?¶
An altered expression can reveal unexpected nodes or bypass a selection rule. If the document contains sensitive attributes, an overly broad lookup can expose them. Do not store plaintext passwords in XML and compare them through XPath as an authentication mechanism.
Recommendation¶
Use a fixed XPath expression with variables supported by the library, or navigate parsed nodes and compare values as data. Keep XPath source, namespace mappings and allowed query choices under application control. Replacing a quote with ' is not a general-purpose XPath parameterization method and changes the intended data comparison.
Parse XML with XXE and resource protections before evaluating XPath. Restrict the data loaded into the document and apply object authorization to the result. Input type/length checks remain useful but do not replace the expression/data separation.
Sample Code¶
All examples use an already parsed, bounded document with structure <customers><customer name="..."/></customers>. The document parser must be configured safely. The unsafe form is "/customers/customer[@name='" + name + "']".
var xpath = javax.xml.xpath.XPathFactory.newInstance().newXPath();
xpath.setXPathVariableResolver(variable -> {
if ("name".equals(variable.getLocalPart()) && variable.getNamespaceURI().isEmpty()) {
return name;
}
throw new IllegalArgumentException("Unknown XPath variable");
});
var expression = xpath.compile("/customers/customer[@name=$name]");
var matches = (org.w3c.dom.NodeList) expression.evaluate(
document, javax.xml.xpath.XPathConstants.NODESET);
lxml variable binding:
def find_customers(document, name: str):
if not isinstance(name, str) or not 1 <= len(name) <= 100:
raise ValueError("Invalid name")
return document.xpath("/customers/customer[@name=$name]", name=name)
document must come from a hardened parser; this function does not load XML or resolve URLs.
LINQ to XML avoids constructing an XPath expression for this simple lookup:
// document is an XDocument; import System.Linq and System.Xml.Linq.
var matches = document.Root!
.Elements("customer")
.Where(customer => (string?)customer.Attribute("name") == name)
.ToList();
Validate the expected root element before this fragment. If namespaces are used, match the intended XName; do not silently ignore them.
Native DOMXPath does not offer the same variable-binding API as every other XPath library. Select with a fixed expression, then compare the attribute value:
$xpath = new DOMXPath($document);
$matches = [];
foreach ($xpath->query('/customers/customer') as $customer) {
if ($customer instanceof DOMElement && $customer->getAttribute('name') === $name) {
$matches[] = $customer;
}
}
Keep documents bounded; loading a very large tree merely to filter it is a resource risk.
The xpath package supports variables through its parsed-expression API:
import xpath from 'xpath';
const expression = xpath.parse('/customers/customer[@name=$name]');
const matches = expression.select({ node: document, variables: { name } });
Use a maintained XML DOM parser with the appropriate XXE controls. A different XPath package may have a different variable API.
Regression checks¶
Use local XML fixtures with names containing an apostrophe, double quote, Unicode and XPath-looking punctuation. Each input should select only its exact record, not change the expression. Test a missing match, multiple legitimate matches and namespaces. Confirm XML input limits and XXE rejection separately from the XPath checks.