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

ImpactDetails
ConfidentialityScope: Data Disclosure

Unauthorized access to XML data.
AuthenticationScope: Bypass

Login mechanisms circumvented.
IntegrityScope: 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


CVE Examples

  • CVE-2007-1253: XPath injection in XML login.

  • CVEs in various XML-processing applications.


References

  1. MITRE. "CWE-643: Improper Neutralization of Data within XPath Expressions." https://cwe.mitre.org/data/definitions/643.html

  2. OWASP. "XPath Injection."