Unsachgemäße Neutralisierung von Daten innerhalb von XPath-Ausdrücken (XPath-Injection)
Beschreibung
XPath-Injection tritt auf, wenn benutzergelieferte Daten in XPath-Abfragen eingebunden werden, ohne ordnungsgemäße Sanitisierung. XPath wird verwendet, um XML-Dokumente abzufragen, und wie SQL kann es manipuliert werden, wenn Benutzereingaben direkt in Abfragen konkateniert werden. Angreifer können die XPath-Abfragelogik modifizieren, um Authentifizierung zu umgehen, unbefugte Daten zu extrahieren oder die Struktur von XML-Dokumenten zu sondieren.
Risiko
Authentifizierungs-Bypass wenn XPath für Credential-Verifizierung verwendet wird. Extraktion sensibler Daten aus XML-Dokumenten. Offenlegung der XML-Dokumentstruktur. Blind-XPath-Injection für Datenaufzählung. Denial of Service durch komplexe Abfragen. In einigen Implementierungen Zugriff auf Dateisystem oder Code-Ausführung.
Lösung
Verwenden Sie parametrisierte XPath-Abfragen, wo verfügbar. Validieren und sanitisieren Sie alle Benutzereingaben vor Einbindung in XPath. Escapen Sie spezielle XPath-Zeichen. Verwenden Sie Whitelisting für erlaubte Eingabemuster. Implementieren Sie Eingabelängenlimits. Erwägen Sie die Verwendung von XPath 2.0's vorbereiteten Ausdrücken, wo unterstützt.
Häufige Auswirkungen
| Auswirkung | Details |
|---|---|
| Vertraulichkeit | Bereich: Datenoffenlegung Unbefugter Zugriff auf XML-Daten. |
| Authentifizierung | Bereich: Bypass Login-Mechanismen umgangen. |
| Integrität | Bereich: Abfragemanipulation Unbeabsichtigte XPath-Operationen ausgeführt. |
Beispielcode + Lösungscode
Verwundbarer Code
// VERWUNDBAR: XPath-Injection in Authentifizierung
public class VulnerableXPathAuth {
private Document usersDoc;
public boolean authenticate(String username, String password) {
// Verwundbar: direkte String-Konkatenation
String xpath = "//users/user[username='" + username +
"' and password='" + password + "']";
// Angriff: username = "admin' or '1'='1' or 'x'='"
// Angriff: password = "' or '1'='1"
// Ergibt: //users/user[username='admin' or '1'='1' or 'x'='' and password='' or '1'='1']
// Das gibt immer true zurück!
XPath xPath = XPathFactory.newInstance().newXPath();
NodeList nodes = (NodeList) xPath.evaluate(xpath, usersDoc,
XPathConstants.NODESET);
return nodes.getLength() > 0;
}
public String getUserEmail(String userId) {
// Verwundbar: Benutzer-ID in XPath
String xpath = "//users/user[@id='" + userId + "']/email";
// Angriff: userId = "1'] | //users/user/password | //users/user[@id='1"
// Extrahiert alle Passwörter!
return xPathQuery(xpath);
}
}
// VERWUNDBAR: Datenextraktion
public class VulnerableXMLSearch {
public List<String> searchProducts(String category) {
// Verwundbare Abfrage
String xpath = "//products/product[category='" + category + "']/name";
// Angriff: category = "'] | //users/user/creditcard | //products/product[category='"
// Extrahiert Kreditkartendaten!
return executeXPath(xpath);
}
}
# VERWUNDBAR: 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):
# Verwundbar: direkte String-Formatierung
xpath = f"//users/user[name='{username}' and pass='{password}']"
# Angriff: username = "' or '1'='1"
# Angriff: password = "' or '1'='1"
result = self.tree.xpath(xpath)
return len(result) > 0
def get_user_data(self, username):
# Verwundbare Extraktion
xpath = f"//users/user[name='{username}']/data"
# Angriff: username = "'] | //* | //users[name='"
# Gibt ganzes Dokument zurück!
return self.tree.xpath(xpath)
def search_items(self, search_term):
# Verwundbare Suche
xpath = f"//items/item[contains(name, '{search_term}')]"
# Angriff: search_term = "')] | //secret/data | //items/item[contains(name, '"
return self.tree.xpath(xpath)
# VERWUNDBAR: Blind-XPath-Injection
def check_user_exists(username):
xpath = f"//users/user[name='{username}']"
# Angriff: Boolean-basierte Injection verwenden, um Daten zu extrahieren
# username = "admin' and string-length(password)>5 and 'a'='"
# Gibt true/false basierend auf Passwortlänge zurück
result = tree.xpath(xpath)
return len(result) > 0
// VERWUNDBAR: 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) {
// Verwundbare Abfragekonstruktion
const query = `//users/user[username='${username}' and password='${password}']`;
// Angriff: username = "admin' or '1'='1"
const nodes = xpath.select(query, this.doc);
return nodes.length > 0;
}
findUserByRole(role) {
// Verwundbar für Injection
const query = `//users/user[role='${role}']/name/text()`;
// Angriff: role = "admin'] | //users/user/password | //users/user[role='admin"
return xpath.select(query, this.doc);
}
searchContent(keyword) {
// Verwundbare Suche
const query = `//content/article[contains(text(), '${keyword}')]`;
// Angriff: keyword = "')] | //config/database | //content[contains(text(), '"
return xpath.select(query, this.doc);
}
}
<?php
// VERWUNDBAR: PHP XPath-Injection
class VulnerableXPathAuth {
private $xml;
public function __construct($xmlFile) {
$this->xml = simplexml_load_file($xmlFile);
}
public function login($username, $password) {
// Verwundbar: direkte Konkatenation
$xpath = "//users/user[username='$username' and password='$password']";
// Angriff: username = "' or ''='"
// Angriff: password = "' or ''='"
$result = $this->xml->xpath($xpath);
return count($result) > 0;
}
public function getUser($userId) {
// Verwundbare Benutzersuche
$xpath = "//users/user[@id='$userId']";
// Angriff: userId = "1' or '1'='1"
return $this->xml->xpath($xpath);
}
public function search($term) {
// Verwundbare Suche
$xpath = "//items/item[contains(name, '$term')]";
// Angriff: term = "')] | //sensitive/* | //items[contains(name, '"
return $this->xml->xpath($xpath);
}
}
// Verwendung zeigt Schwachstelle
$auth = new VulnerableXPathAuth('users.xml');
$username = $_POST['username'];
$password = $_POST['password'];
if ($auth->login($username, $password)) {
// Angreifer umgeht Authentifizierung!
echo "Willkommen!";
}
?>
Lösungscode
// SICHER: XPath mit ordnungsgemäßer Eingabebehandlung
public class SafeXPathAuth {
private Document usersDoc;
// Spezielle XPath-Zeichen escapen
private String escapeXPath(String input) {
if (input == null) return "";
// Wenn beide Anführungszeichen enthält, concat verwenden
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();
}
// Entgegengesetzten Anführungszeichentyp verwenden
if (input.contains("'")) {
return "\"" + input + "\"";
}
return "'" + input + "'";
}
public boolean authenticate(String username, String password) {
// Eingabevalidierung
if (!isValidUsername(username) || password == null) {
return false;
}
// Escaped Werte verwenden
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; // Sicher fehlschlagen
}
}
// Besser: Parametrisierten Ansatz mit XPath-Variablen verwenden
public boolean authenticateParameterized(String username, String password) {
// Eingabe validieren
if (!isValidInput(username) || !isValidInput(password)) {
return false;
}
XPath xPath = XPathFactory.newInstance().newXPath();
// Variable-Resolver für Parameter verwenden
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-Validierung
return input != null &&
input.length() <= 50 &&
input.matches("^[[email protected]]+$");
}
}
# SICHER: Python XPath mit ordnungsgemäßem 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):
"""String für sichere XPath-Einbindung escapen."""
if "'" not in value:
return f"'{value}'"
elif '"' not in value:
return f'"{value}"'
else:
# concat für Strings mit beiden Anführungszeichentypen verwenden
parts = value.split("'")
return "concat('" + "', \"'\", '".join(parts) + "')"
def validate_input(self, value, max_length=50):
"""Eingabe validieren und sanitisieren."""
if not value or len(value) > max_length:
return None
# Sichere Zeichen whitelisten
if not re.match(r'^[a-zA-Z0-9_@.\- ]+$', value):
return None
return value
def login(self, username, password):
# Eingabe validieren
username = self.validate_input(username)
password = self.validate_input(password)
if not username or not password:
return False
# Werte escapen
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 # Sicher fehlschlagen
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: XPath mit Parametern verwenden (lxml-Erweiterung)
def search_items_safe(self, search_term):
search_term = self.validate_input(search_term)
if not search_term:
return []
# XPath-Variablensubstitution verwenden
xpath = "//items/item[contains(name, $term)]"
result = self.tree.xpath(xpath, term=search_term)
return result
// SICHER: Node.js XPath mit 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}"`;
}
// concat für Strings mit beiden Anführungszeichentypen verwenden
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;
}
// Alphanumerisch und grundlegende Interpunktion whitelisten
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; // Sicher fehlschlagen
}
}
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
// SICHER: PHP XPath mit ordnungsgemäßem 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 . '"';
}
// concat für Strings mit beiden Anführungszeichen verwenden
$parts = explode("'", $value);
return "concat('" . implode("', \"'\", '", $parts) . "')";
}
private function validateInput($value, $maxLength = 50) {
if (!$value || strlen($value) > $maxLength) {
return null;
}
// Sichere Zeichen whitelisten
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; // Sicher fehlschlagen
}
}
public function getUser($userId) {
// Als Integer validieren
$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;
}
}
// Sichere Verwendung
$auth = new SafeXPathAuth('users.xml');
$username = $_POST['username'] ?? '';
$password = $_POST['password'] ?? '';
if ($auth->login($username, $password)) {
echo "Willkommen!";
} else {
echo "Ungültige Anmeldedaten";
}
?>
Ausgenutzt in der Praxis
Authentifizierungs-Bypass
XPath-Injection zum Umgehen XML-basierter Login-Systeme.
Datenextraktion
Extraktion sensibler XML-Daten durch Injection.
SAML-Angriffe
XPath-Injection in SAML-Authentifizierungsabläufen.
Tools zum Testen/Ausnutzen
- XPath Injection Exploiter
- Burp Suite — XPath-Fuzzing.
- Manuelle Injection-Tests.
CVE-Beispiele
- CVE-2007-1253: XPath-Injection in XML-Login.
- CVEs in verschiedenen XML-verarbeitenden Anwendungen.
Referenzen
- MITRE. "CWE-643: Improper Neutralization of Data within XPath Expressions." https://cwe.mitre.org/data/definitions/643.html
- OWASP. "XPath Injection."