Engineering · Aug 27, 2026 · 7 min read

XPath injection: boolean and blind oracles confirm the parse, not the path

XPath 1.0 has no parameterized queries - user input concatenated into a tree-parse expression carries the same bypass potential as SQL injection, with distinct error signals per library and a boolean differential as the confirming artifact.

XPath injection exploits the same root cause as SQL injection - user input concatenated into a query string - but targets a tree parser rather than a relational database. The result looks different: no error messages citing table names, no information_schema to mine. What stays the same is the oracle: a boolean predicate that flips a conditional from false to true proves the parser evaluated your input, not the application.

01XPath 1.0 has no parameterized queries

XPath 1.0 - still the dominant standard in SOAP services, XML-backed user stores and configuration-driven logic - takes a raw string. The library evaluates whatever expression it receives. When user input is concatenated into that string without escaping, the query changes meaning. There is no equivalent of a prepared statement at the XPath level: the only way to prevent injection is to validate and escape input before it reaches the expression builder, or to restructure the query so user-supplied values appear only inside fixed, quoted literals that cannot change the predicate.

Application builds: /users/user[name='INPUT']/password Legitimate input: alice Expression: /users/user[name='alice']/password selects the alice node ──► expected Injected input: '] or '1'='1 Expression becomes: /users/user[name='' or '1'='1] /password matches every user node ──► bypass
The predicate or '1'='1 is always true, so every user node matches - the same logic as SQL auth bypass but evaluated by the XML engine with no SQL keyword in sight.

The most common vectors are login forms backed by an XML user store, SOAP services that embed user input in XPath expressions for data retrieval, and application configuration files read with embedded query parameters. Any place a string value flows from user input into an XPath expression without escaping is a candidate surface.

02The confirming artifact is a boolean differential

Confirming XPath injection follows the same differential discipline as every other oracle probe: send a payload that changes result cardinality without touching or disclosing any real data, then compare the two responses. For an authentication context the confirming artifact is a 200 where a baseline wrong credential returns 401 - with the injected predicate documented and no real user credential required:

xpath-probe.httphttp
# baseline: legitimate wrong-credential attempt
POST /api/login
Content-Type: application/x-www-form-urlencoded

username=probe&password=probe
HTTP/1.1 401 Unauthorized

# boolean injection: predicate appended in string-literal context
POST /api/login
Content-Type: application/x-www-form-urlencoded

username=probe%27%5D+or+%271%27%3D%271&password=x
# decoded: probe'] or '1'='1
HTTP/1.1 200 OK   Set-Cookie: session=...   # predicate evaluated - boundary broke

If there is no authentication flow to probe, a blind boolean oracle works on any endpoint that returns a discernibly different response for a true versus false predicate. Injecting a condition that evaluates against a known structural fact - such as whether a node count exceeds a threshold - and comparing response length or status code produces the same differential evidence. Timing is less reliable because XPath evaluation offers no sleep equivalent; behavioral differences on result cardinality are the primary signal.

Proven - CriticalXPath injection - authentication bypass - /api/login
POST /api/login username=probe'] or '1'='1&password=x ──► 200 OK Set-Cookie: session=abcdef (valid session) baseline: username=probe&password=probe ──► 401 Unauthorized Differential holds across 5 consecutive repeats.
Oracle: boolean differential. No records written; no real user credential used. Nonce marker 62615533 embedded in username to scope the probe. Severity critical - unconditional predicate grants a session for any supplied username.

03Library fingerprinting changes the probe

A malformed XPath expression produces different error output depending on the library processing it. Fingerprinting the library before probing avoids false negatives from payloads calibrated to a different engine. The key observable is the error structure the application surfaces when the expression is syntactically invalid - even a sanitized application will often expose the library class name or message prefix in a 500 response or a SOAP fault envelope.

LibraryTypical error signalCommon host
libxml2 (PHP / Python lxml)XPath error: invalid expression - prefix in error bodyPHP DOMXPath, SimpleXML; Python lxml etree
Java javax.xml.xpath / Saxonjavax.xml.xpath.XPathExpressionException in stack trace or SOAP faultSOAP services, Spring XML config, JAXB pipelines
.NET System.XmlXPathException: Expression must evaluate to a node-setASP.NET SOAP endpoints, WCF, XmlDocument.SelectNodes
Ruby NokogiriNokogiri::XML::XPath::SyntaxError in response or log leakRails backends, feed processors, XML configuration readers

Because error messages are often suppressed in production, library fingerprinting from HTTP response headers, server banners and framework cookies is a useful secondary signal. A Java SOAP service frequently reveals its stack through X-Powered-By or a fault envelope even when it suppresses exception bodies. The probe adapts: namespace handling differs between libxml2 and the Java XPath engine, and a single-payload approach creates a silent false negative for every variant it did not model.

04Severity follows confirmed capability

XPath injection severity is set by what the confirmed predicate achieves, not by the injection class alone. An authentication bypass that grants a session is critical - arbitrary authentication bypass is the impact, not the mechanism. A blind boolean oracle that leaks only result-set cardinality, with no path to a session or data read, is high while the extraction path is unconfirmed. Blind data extraction character by character via substring() and boolean conditions is confirmed high to critical depending on what the nodes contain. Structural disclosure that leaks node names but no values is medium, and treated as reachable until the extraction path is closed.

4
libraries, four distinct error signals
0
real credentials used in any probe
1
boolean differential, repeated 5x to confirm

The fix is the same regardless of library: never concatenate user input directly into an XPath expression. For simple lookups, rewrite the query so user-supplied values are compared inside fixed quoted literals using library-specific escaping (apostrophe-doubling per the XPath 1.0 specification, or ESAPI XPathEscaper). Where query complexity demands dynamic path segments, pre-validate that input matches a strict allowlist of known node names before any string is assembled. The confirming request and response attach to the finding; the remediation names the expression builder and the escaping function required.

Evidence over heuristicsReachable beats foundMonitor → Block: rolling out a gate without slowing teamsIaC-grounded threat modelingAir-gapped AppSec with no phone-homeOne platform vs three tools
See it on your own app →