Improper Neutralization of Data within XPath Expressions (XPath Injection)
Description
XPath Injection occurs when user-supplied data is incorporated into XPath queries without proper sanitization. XPath is used to query XML documents, and like SQL, it can be manipulated if user input is directly concatenated into queries. Attackers can modify the XPath query logic to bypass authentication, extract unauthorized data, or probe the structure of XML documents.
Risk
Authentication bypass when XPath is used for credential verification. Extraction of sensitive data from XML documents. Disclosure of XML document structure. Blind XPath injection for data enumeration. Denial of service through complex queries. In some implementations, access to filesystem or code execution.
Solution
Use parameterized XPath queries when available. Validate and sanitize all user input before inclusion in XPath. Escape special XPath characters. Use whitelisting for allowed input patterns. Implement input length limits. Consider using XPath 2.0's prepared expressions where supported.
Common Consequences
| Impact | Details |
|---|---|
| Confidentiality | Scope: Data Disclosure Unauthorized access to XML data. |
| Authentication | Scope: Bypass Login mechanisms circumvented. |
| Integrity | Scope: Query Manipulation Unintended XPath operations executed. |
Example Code + Solution Code
Vulnerable Code
// VULNERABLE: XPath injection in authentication
public class VulnerableXPathAuth {
private Document usersDoc;
public boolean authenticate(String username, String password) {
// Vulnerable: direct string concatenation
String xpath = "//users/user[username='" + username +
"' and password='" + password + "']";
// Attack: username = "admin' or '1'='1' or 'x'='"
// Attack: password = "' or '1'='1"
// Results in: //users/user[username='admin' or '1'='1' or 'x'='' and password='' or '1'='1']
// Which always returns true!
XPath xPath = XPathFactory.newInstance().newXPath();
NodeList nodes = (NodeList) xPath.evaluate(xpath, usersDoc,
XPathConstants.NODESET);
return nodes.getLength() > 0;
}
public String getUserEmail(String userId) {
// Vulnerable: user ID in XPath
String xpath = "//users/user[@id='" + userId + "']/email";
// Attack: userId = "1'] | //users/user/password | //users/user[@id='1"
// Extracts all passwords!
return xPathQuery(xpath);
}
}
// VULNERABLE: Data extraction
public class VulnerableXMLSearch {
public List<String> searchProducts(String category) {
// Vulnerable query
String xpath = "//products/product[category='" + category + "']/name";
// Attack: category = "'] | //users/user/creditcard | //products/product[category='"
// Extracts credit card data!
return executeXPath(xpath);
}
}
# VULNERABLE: Python XPath injection
from lxml import etree
class VulnerableXPathHandler:
def __init__(self, xml_file):
self.tree = etree.parse(xml_file)
def login(self, username, password):
# Vulnerable: direct string formatting
xpath = f"//users/user[name='{username}' and pass='{password}']"
# Attack: username = "' or '1'='1"
# Attack: password = "' or '1'='1"
result = self.tree.xpath(xpath)
return len(result) > 0
def get_user_data(self, username):
# Vulnerable extraction
xpath = f"//users/user[name='{username}']/data"
# Attack: username = "'] | //* | //users[name='"
# Returns entire document!
return self.tree.xpath(xpath)
def search_items(self, search_term):
# Vulnerable search
xpath = f"//items/item[contains(name, '{search_term}')]"
# Attack: search_term = "')] | //secret/data | //items/item[contains(name, '"
return self.tree.xpath(xpath)
# VULNERABLE: Blind XPath injection
def check_user_exists(username):
xpath = f"//users/user[name='{username}']"
# Attack: Use boolean-based injection to extract data
# username = "admin' and string-length(password)>5 and 'a'='"
# Returns true/false based on password length
result = tree.xpath(xpath)
return len(result) > 0
// VULNERABLE: Node.js XPath injection
const xpath = require('xpath');
const dom = require('xmldom').DOMParser;
class VulnerableXMLService {
constructor(xmlContent) {
this.doc = new dom().parseFromString(xmlContent);
}
authenticate(username, password) {
// Vulnerable query construction
const query = `//users/user[username='${username}' and password='${password}']`;
// Attack: username = "admin' or '1'='1"
const nodes = xpath.select(query, this.doc);
return nodes.length > 0;
}
findUserByRole(role) {
// Vulnerable to injection
const query = `//users/user[role='${role}']/name/text()`;
// Attack: role = "admin'] | //users/user/password | //users/user[role='admin"
return xpath.select(query, this.doc);
}
searchContent(keyword) {
// Vulnerable search
const query = `//content/article[contains(text(), '${keyword}')]`;
// Attack: keyword = "')] | //config/database | //content[contains(text(), '"
return xpath.select(query, this.doc);
}
}
<?php
// VULNERABLE: PHP XPath injection
class VulnerableXPathAuth {
private $xml;
public function __construct($xmlFile) {
$this->xml = simplexml_load_file($xmlFile);
}
public function login($username, $password) {
// Vulnerable: direct concatenation
$xpath = "//users/user[username='$username' and password='$password']";
// Attack: username = "' or ''='"
// Attack: password = "' or ''='"
$result = $this->xml->xpath($xpath);
return count($result) > 0;
}
public function getUser($userId) {
// Vulnerable user lookup
$xpath = "//users/user[@id='$userId']";
// Attack: userId = "1' or '1'='1"
return $this->xml->xpath($xpath);
}
public function search($term) {
// Vulnerable search
$xpath = "//items/item[contains(name, '$term')]";
// Attack: term = "')] | //sensitive/* | //items[contains(name, '"
return $this->xml->xpath($xpath);
}
}
// Usage showing vulnerability
$auth = new VulnerableXPathAuth('users.xml');
$username = $_POST['username'];
$password = $_POST['password'];
if ($auth->login($username, $password)) {
// Attacker bypasses authentication!
echo "Welcome!";
}
?>
Fixed Code
// SAFE: XPath with proper input handling
public class SafeXPathAuth {
private Document usersDoc;
// Escape special XPath characters
private String escapeXPath(String input) {
if (input == null) return "";
// If contains both quotes, use concat
if (input.contains("'") && input.contains("\"")) {
StringBuilder sb = new StringBuilder("concat(");
String[] parts = input.split("'");
for (int i = 0; i < parts.length; i++) {
if (i > 0) sb.append(",\"'\",");
sb.append("'").append(parts[i]).append("'");
}
sb.append(")");
return sb.toString();
}
// Use opposite quote type
if (input.contains("'")) {
return "\"" + input + "\"";
}
return "'" + input + "'";
}
public boolean authenticate(String username, String password) {
// Input validation
if (!isValidUsername(username) || password == null) {
return false;
}
// Use escaped values
String escapedUser = escapeXPath(username);
String escapedPass = escapeXPath(password);
String xpath = "//users/user[username=" + escapedUser +
" and password=" + escapedPass + "]";
XPath xPath = XPathFactory.newInstance().newXPath();
try {
NodeList nodes = (NodeList) xPath.evaluate(xpath, usersDoc,
XPathConstants.NODESET);
return nodes.getLength() > 0;
} catch (XPathExpressionException e) {
return false; // Fail securely
}
}
// Better: Use parameterized approach with XPath variables
public boolean authenticateParameterized(String username, String password) {
// Validate input
if (!isValidInput(username) || !isValidInput(password)) {
return false;
}
XPath xPath = XPathFactory.newInstance().newXPath();
// Use variable resolver for parameters
xPath.setXPathVariableResolver(variableName -> {
if ("username".equals(variableName.getLocalPart())) {
return username;
}
if ("password".equals(variableName.getLocalPart())) {
return password;
}
return null;
});
String xpath = "//users/user[username=$username and password=$password]";
try {
NodeList nodes = (NodeList) xPath.evaluate(xpath, usersDoc,
XPathConstants.NODESET);
return nodes.getLength() > 0;
} catch (XPathExpressionException e) {
return false;
}
}
private boolean isValidInput(String input) {
// Whitelist validation
return input != null &&
input.length() <= 50 &&
input.matches("^[[email protected]]+$");
}
}
# SAFE: Python XPath with proper escaping
from lxml import etree
import re
class SafeXPathHandler:
def __init__(self, xml_file):
self.tree = etree.parse(xml_file)
def escape_xpath_string(self, value):
"""Escape string for safe XPath inclusion."""
if "'" not in value:
return f"'{value}'"
elif '"' not in value:
return f'"{value}"'
else:
# Use concat for strings with both quote types
parts = value.split("'")
return "concat('" + "', \"'\", '".join(parts) + "')"
def validate_input(self, value, max_length=50):
"""Validate and sanitize input."""
if not value or len(value) > max_length:
return None
# Whitelist safe characters
if not re.match(r'^[a-zA-Z0-9_@.\- ]+$', value):
return None
return value
def login(self, username, password):
# Validate input
username = self.validate_input(username)
password = self.validate_input(password)
if not username or not password:
return False
# Escape values
safe_user = self.escape_xpath_string(username)
safe_pass = self.escape_xpath_string(password)
xpath = f"//users/user[name={safe_user} and pass={safe_pass}]"
try:
result = self.tree.xpath(xpath)
return len(result) > 0
except etree.XPathError:
return False # Fail securely
def get_user_data(self, username):
username = self.validate_input(username)
if not username:
return []
safe_user = self.escape_xpath_string(username)
xpath = f"//users/user[name={safe_user}]/data"
try:
return self.tree.xpath(xpath)
except etree.XPathError:
return []
# BEST: Use XPath with parameters (lxml extension)
def search_items_safe(self, search_term):
search_term = self.validate_input(search_term)
if not search_term:
return []
# Use XPath variable substitution
xpath = "//items/item[contains(name, $term)]"
result = self.tree.xpath(xpath, term=search_term)
return result
// SAFE: Node.js XPath with escaping
const xpath = require('xpath');
const dom = require('xmldom').DOMParser;
class SafeXMLService {
constructor(xmlContent) {
this.doc = new dom().parseFromString(xmlContent);
}
escapeXPathString(value) {
if (!value.includes("'")) {
return `'${value}'`;
}
if (!value.includes('"')) {
return `"${value}"`;
}
// Use concat for strings with both quote types
const parts = value.split("'");
return "concat('" + parts.join("', \"'\", '") + "')";
}
validateInput(value, maxLength = 50) {
if (!value || typeof value !== 'string') {
return null;
}
if (value.length > maxLength) {
return null;
}
// Whitelist alphanumeric and basic punctuation
if (!/^[a-zA-Z0-9_@.\- ]+$/.test(value)) {
return null;
}
return value;
}
authenticate(username, password) {
username = this.validateInput(username);
password = this.validateInput(password);
if (!username || !password) {
return false;
}
const safeUser = this.escapeXPathString(username);
const safePass = this.escapeXPathString(password);
const query = `//users/user[username=${safeUser} and password=${safePass}]`;
try {
const nodes = xpath.select(query, this.doc);
return nodes.length > 0;
} catch (e) {
return false; // Fail securely
}
}
findUserByRole(role) {
role = this.validateInput(role);
if (!role) {
return [];
}
const safeRole = this.escapeXPathString(role);
const query = `//users/user[role=${safeRole}]/name/text()`;
try {
return xpath.select(query, this.doc);
} catch (e) {
return [];
}
}
}
<?php
// SAFE: PHP XPath with proper escaping
class SafeXPathAuth {
private $xml;
public function __construct($xmlFile) {
$this->xml = simplexml_load_file($xmlFile);
}
private function escapeXPath($value) {
if (strpos($value, "'") === false) {
return "'" . $value . "'";
}
if (strpos($value, '"') === false) {
return '"' . $value . '"';
}
// Use concat for strings with both quotes
$parts = explode("'", $value);
return "concat('" . implode("', \"'\", '", $parts) . "')";
}
private function validateInput($value, $maxLength = 50) {
if (!$value || strlen($value) > $maxLength) {
return null;
}
// Whitelist safe characters
if (!preg_match('/^[a-zA-Z0-9_@.\- ]+$/', $value)) {
return null;
}
return $value;
}
public function login($username, $password) {
$username = $this->validateInput($username);
$password = $this->validateInput($password);
if (!$username || !$password) {
return false;
}
$safeUser = $this->escapeXPath($username);
$safePass = $this->escapeXPath($password);
$xpath = "//users/user[username=$safeUser and password=$safePass]";
try {
$result = $this->xml->xpath($xpath);
return count($result) > 0;
} catch (Exception $e) {
return false; // Fail securely
}
}
public function getUser($userId) {
// Validate as integer
$userId = filter_var($userId, FILTER_VALIDATE_INT);
if ($userId === false) {
return null;
}
$xpath = "//users/user[@id='$userId']";
$result = $this->xml->xpath($xpath);
return count($result) > 0 ? $result[0] : null;
}
}
// Safe usage
$auth = new SafeXPathAuth('users.xml');
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
if ($auth->login($username, $password)) {
echo "Welcome!";
} else {
echo "Invalid credentials";
}
?>
Exploited in the Wild
Authentication Bypass
XPath injection to bypass XML-based login systems.
Data Extraction
Extracting sensitive XML data through injection.
SAML Attacks
XPath injection in SAML authentication flows.
Tools to test/exploit
-
Burp Suite — XPath fuzzing.
-
Manual injection testing.
CVE Examples
-
CVE-2007-1253: XPath injection in XML login.
-
CVEs in various XML-processing applications.
References
-
MITRE. "CWE-643: Improper Neutralization of Data within XPath Expressions." https://cwe.mitre.org/data/definitions/643.html
-
OWASP. "XPath Injection."