XPath injection

Java XPath injection and variable binding

Description

XPath expressions can contain conditions, functions, and operators as well as paths that select XML nodes. Concatenating or directly passing request strings into the expression argument of compile, evaluate, evaluateExpression, dom4j selection methods, or Jaxen expression constructors lets an attacker control query structure instead of supplying a data value.

Keep expressions separate from data. In xpath.evaluate(expression, document), the first argument is the expression and the second is the context to query. Declare external values as variables in a fixed expression and supply them through XPathVariableResolver or a dom4j/Jaxen VariableContext.

XPath 1.0 string literals have no universal escape syntax for arbitrary input. Removing quotes, using a generic escape or sanitize helper, applying a format regex, or precompiling the expression does not separate data from expression structure.

Potential impact

  • Unauthorized XML access: Changed paths or conditions can select nodes outside the intended filter.
  • Security-check bypass: If XPath results drive authentication, authorization, or policy decisions, an attacker may influence those decisions.
  • Resource consumption: When an application accepts whole expressions, complex paths and operations can make evaluation expensive. Using XPath alone does not imply a denial-of-service vulnerability.

Remediation

The central rule is to define XPath expressions on the server and bind external input only as data values.

  • In Java XPath, use $name variables in a fixed expression and supply values through XPath#setXPathVariableResolver. Set the resolver before compiling or evaluating the expression.
  • In dom4j and Jaxen, combine a fixed expression with a VariableContext.
  • If users must choose structure that variables cannot represent, such as element names, axes, or predicate fragments, map an external key to a small finite set of complete server-defined expressions. Do not append the input string itself.
  • Use input validation for business constraints such as length or identifier format, not as a substitute for variable binding.
  • Calling compile after constructing an untrusted expression is not a safeguard: compilation interprets that string as XPath syntax.

Examples

Java XPath

Before

The request value is inserted between quotes, so profileId can change the condition's structure.

java
import jakarta.servlet.http.HttpServletRequest;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

final class VulnerableXPathLookup {
    static Node findProfile(HttpServletRequest request, Document document)
            throws XPathExpressionException {
        String profileId = request.getParameter("profileId");
        String expression = "/directory/profile[@id='" + profileId + "']";

        XPath xpath = XPathFactory.newInstance().newXPath();
        return xpath.evaluateExpression(expression, document, Node.class);
    }
}

After

The expression stays constant and the request value is bound as a variable. Quotes or XPath tokens in the value remain data rather than expression syntax.

java
import jakarta.servlet.http.HttpServletRequest;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;

final class SafeXPathLookup {
    static Node findProfile(HttpServletRequest request, Document document)
            throws XPathExpressionException {
        String profileId = request.getParameter("profileId");

        XPath xpath = XPathFactory.newInstance().newXPath();
        xpath.setXPathVariableResolver(variable -> switch (variable.getLocalPart()) {
            case "profileId" -> profileId;
            default -> null;
        });

        String expression = "/directory/profile[@id=$profileId]";
        return xpath.evaluateExpression(expression, document, Node.class);
    }
}

XPath is neither thread-safe nor reentrant. Do not repeatedly change a request-specific resolver on a shared XPath object. Use request-scoped objects as shown, or manage concurrent access safely.

dom4j/Jaxen variable binding

dom4j can accept Jaxen's VariableContext when creating an XPath expression.

java
import org.dom4j.Document;
import org.dom4j.DocumentHelper;
import org.dom4j.Node;
import org.dom4j.XPath;
import org.jaxen.SimpleVariableContext;

final class SafeDom4jXPathLookup {
    static Node findProfile(Document document, String profileId) {
        SimpleVariableContext variables = new SimpleVariableContext();
        variables.setVariableValue("profileId", profileId);

        XPath xpath = DocumentHelper.createXPath(
                "/directory/profile[@id=$profileId]", variables);
        return xpath.selectSingleNode(document);
    }
}

Selecting server-defined expressions

XPath variables represent data, not syntax such as element names or axes. When users must choose such structure, select a complete server-owned expression instead of validating and concatenating their input.

java
final class ServerOwnedXPathView {
    static String expressionFor(String requestedView) {
        return switch (requestedView) {
            case "active" -> "/directory/profile[@active='true']";
            case "locked" -> "/directory/profile[@locked='true']";
            default -> "/directory/profile";
        };
    }
}

The engine always receives one of the three expressions defined in code. requestedView itself is never inserted into the XPath string.

Arbitrary XPath features and resource limits

If a product requires complete XPath input, such as an administrator diagnostic tool, generic sanitization cannot make meaningful arbitrary expressions safe. Isolate the feature, enforce strong authorization, provide only the required XML data, and limit request frequency and result size.

Java SE 25's jdk.xml.xpathExprGrpLimit and jdk.xml.xpathExprOpLimit limit XPath groups and operators. Keep them within the application's requirements. These limits reduce resource-consumption risk as defense in depth; they do not remove expression injection.

The stable releases checked on September 1, 2026 were dom4j 2.2.0 and Jaxen 2.0.6. Jaxen 2.0.5 replaced recursive core-parser paths with an iterative algorithm and converted remaining stack overflows to JaxenException; 2.0.6 corrected parenthesized-expression evaluation in that algorithm. Dependency updates matter, but they do not restrict the meaning of attacker-selected expressions.

References