Safeguard
Application Security

XPath injection: how it happens and how to stop it in Java, .NET, and PHP

A 2024 GeoServer flaw showed unsanitized input reaching an XPath evaluator can mean remote code execution, not just data leakage. Here's how to prevent it.

Safeguard Research Team
Research
6 min read

XPath injection is the XML-document analog of SQL injection, and it has produced consequences well beyond authentication bypass. CVE-2024-36401, disclosed in 2024, showed GeoServer and GeoTools passing user-controlled property names from WFS/WMS/WPS requests straight into Apache Commons JXPath, which evaluated them as XPath expressions — and JXPath's extension-function support let attackers invoke arbitrary Java methods, turning a data-access bug into remote code execution. That is a sharper outcome than the classic textbook example, a login form vulnerable to a payload like ' or '1'='1, but the root cause is identical: untrusted input concatenated directly into an XPath expression string instead of being passed as a bound value. OWASP has tracked this attack class since at least the mid-2000s under CWE-643, "Improper Neutralization of Data within XPath Expressions," and the fix has been stable for just as long — parameterized queries, not string-building. This post walks through how the injection happens, why it's easy to introduce by accident in Java, .NET, and PHP XML stacks, and the concrete parameterization and validation patterns that close it off.

How does XPath injection actually work?

XPath injection works by exploiting the same weakness as SQL injection: an application builds a query by splicing untrusted input into a string, and the query engine can't tell attacker-supplied syntax from the developer's intended structure. A typical vulnerable login check looks like //user[username/text()='" + user + "' and password/text()='" + pass + "']. Supply ' or '1'='1 as the username and the expression becomes always-true, returning the first node regardless of credentials. Because XPath has no built-in blind-output suppression, attackers can also extract data node by node using boolean and timing inference — asking "does the third character of the admin password equal 'a'?" repeatedly until the whole document is reconstructed, per OWASP's community documentation on the attack. Unlike SQL, XPath queries often run against configuration files, SAML assertions, and SOAP payloads, so an injectable XPath sink can expose credentials, session tokens, or business data that never touches a database at all.

Why is XPath injection easy to introduce by accident?

XPath injection is easy to introduce because XML-processing APIs in every major language default to string-based query construction, and it looks identical to normal, safe-seeming code. Java's javax.xml.xpath.XPath.evaluate(String, ...) accepts a raw string; .NET's XmlNode.SelectSingleNode and SelectNodes take a query string; PHP's DOMXPath::query() and SimpleXML's xpath() do the same. None of these APIs warn you when you concatenate a request parameter into that string — there's no equivalent of a prepared-statement placeholder shown by default in tutorials, so developers reach for + or string interpolation the same way they historically did with SQL before ORMs and parameterized drivers became the default teaching pattern. The result: OWASP's Injection Prevention Cheat Sheet treats XPath injection as needing the exact same category of fix as SQL injection — because the underlying mistake, trusting input to be data when it can also be syntax, is the same mistake in a different query language.

How do you prevent it in Java?

In Java, prevent XPath injection by compiling a parameterized expression and binding user input through an XPathVariableResolver instead of splicing strings. Rather than writing xpath.evaluate("//user[username/text()='" + user + "']", doc), define the expression with a variable reference — //user[username/text()=$uname] — and register a resolver via xPath.setXPathVariableResolver(...) that supplies the actual value for $uname at evaluation time. Because the variable is bound as a typed value rather than substituted into expression text, attacker-supplied XPath syntax like a stray ' or or can never change the query's structure. OWASP's Injection Prevention Cheat Sheet documents this pattern explicitly as the Java mitigation for CWE-643, alongside the same defense-in-depth recommendation as SQL injection: validate input types and character sets even when parameterization is in place, since a corrupted node name (not just node value) can still cause unexpected traversal.

How do you prevent it in .NET and PHP?

.NET and PHP lack a first-party bound-variable API as clean as Java's XPathVariableResolver, so the guidance shifts toward strict validation plus fixed query templates. In .NET, avoid building the string passed to SelectSingleNode/SelectNodes from raw request data; if a value must vary, constrain it to an allow-listed type or enumerated set before insertion (e.g., confirm a "username" argument matches ^[A-Za-z0-9_]+$ and has a bounded length) rather than accepting arbitrary text. PHP's DOMXPath::query() and SimpleXML's xpath() carry the identical risk profile — OWASP's cheat sheet groups PHP DOM/SimpleXML XPath queries in the same "avoid string-built" category. The practical pattern for both stacks: treat every XPath query as a fixed template, validate any substituted value against a strict allow-list (type, length, character set) before it ever reaches the query string, and reject anything that fails validation rather than attempting to escape it — escaping XPath metacharacters reliably is harder than it looks and is not the primary recommended control in either ecosystem's cheat sheet guidance.

What does a real exploit chain look like?

CVE-2024-36401 is the clearest recent illustration of how far an XPath sink can go beyond data leakage. GeoServer, an open-source geospatial server, and its underlying GeoTools library evaluated user-supplied property names from OGC web service requests (WFS, WMS, WPS) as XPath expressions through Apache Commons JXPath. Because JXPath supports calling arbitrary Java static methods as XPath extension functions, an attacker who controlled the "property name" input could construct an expression that invoked Runtime.exec or similar — achieving unauthenticated remote code execution, not just query manipulation. The lesson generalizes: any XPath evaluator that supports extension functions turns an injection point into a code-execution primitive, which is a stronger reason to treat XPath sinks with the same rigor as SQL and OS-command sinks, not a lesser one just because the query language sounds less dangerous.

How Safeguard helps

Safeguard's SAST engine traces untrusted input from a source — a request parameter, CLI argument, or file read — through to a dangerous sink such as a query construction call, producing a dataflow trace with CWE/OWASP mapping and a code location rather than a bare pattern match. For Java codebases (one of Safeguard SAST's phase-1 languages alongside JavaScript/TypeScript and Python), that means a string-concatenated call into XPath.evaluate shows up as a traced source-to-sink path a reviewer can act on before merge, the same dataflow model used for SQL and command-injection sinks. On the runtime side, Safeguard DAST detects injection-class findings against verified, in-scope targets using safe, non-destructive checks, and correlates a confirmed runtime finding back to the source-code sink that produced it — so an XPath-adjacent injection surfaced in a running application and its origin in your Java, .NET, or PHP source end up prioritized as one connected finding instead of two disconnected tickets.

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.