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

AuswirkungDetails
VertraulichkeitBereich: Datenoffenlegung

Unbefugter Zugriff auf XML-Daten.
AuthentifizierungBereich: Bypass

Login-Mechanismen umgangen.
IntegritätBereich: 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


CVE-Beispiele

  • CVE-2007-1253: XPath-Injection in XML-Login.
  • CVEs in verschiedenen XML-verarbeitenden Anwendungen.

Referenzen

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