Unzureichende Neutralisierung von Daten in XQuery-Ausdrücken (XQuery-Injection)

Beschreibung

XQuery-Injection tritt auf, wenn vom Benutzer bereitgestellte Daten ohne ordnungsgemäße Bereinigung in XQuery-Ausdrücke eingebunden werden. XQuery ist eine Abfragesprache für XML-Datenbanken, ähnlich wie SQL. Wenn Benutzereingaben direkt in XQuery-Strings konkateniert werden, können Angreifer die Abfragelogik modifizieren, um Authentifizierung zu umgehen, nicht autorisierte Daten zu extrahieren oder XML-Datenbankinhalte zu manipulieren.

Risiko

Authentifizierungsumgehung wenn XQuery Anmeldedaten validiert. Nicht autorisierte Datenextraktion aus XML-Datenbanken. XML-Dokument-Enumeration und Strukturentdeckung. Modifikation gespeicherter XML-Daten. Denial of Service durch komplexe Abfragen. Informationsoffenlegung durch Fehlermeldungen. Vollständige Datenbankkompromittierung in schweren Fällen.

Lösung

Verwenden Sie parametrisierte XQuery-Ausdrücke. Validieren und bereinigen Sie alle Benutzereingaben. Escapen Sie spezielle XQuery-Zeichen. Implementieren Sie Eingabe-Whitelisting. Verwenden Sie XQuery-Bibliotheken, die Variablenbindung unterstützen. Begrenzen Sie Abfragefähigkeiten für nicht vertrauenswürdige Eingaben. Implementieren Sie ordnungsgemäße Fehlerbehandlung, um Informationslecks zu verhindern.

Häufige Konsequenzen

AuswirkungDetails
VertraulichkeitUmfang: Datenoffenlegung

Nicht autorisierter Zugriff auf XML-Daten.
AuthentifizierungUmfang: Umgehung

Authentifizierungslogik umgangen.
IntegritätUmfang: Datenmanipulation

XML-Dokumente ohne Autorisierung modifiziert.

Beispielcode + Lösungscode

Anfälliger Code

// ANFÄLLIG: XQuery-Injection bei Authentifizierung
public class VulnerableXQueryAuth {

    private XQConnection connection;

    public boolean authenticate(String username, String password) {
        // ANFÄLLIG: Direkte String-Konkatenation
        String xquery =
            "for $user in doc('users.xml')//user " +
            "where $user/username = '" + username + "' " +
            "and $user/password = '" + password + "' " +
            "return $user";

        // Angriff: username = "admin' or '1'='1' or 'x'='"
        // Angriff: password = "' or '1'='1"
        // Ergibt Abfrage die immer einen Benutzer zurückgibt

        try {
            XQPreparedExpression expr = connection.prepareExpression(xquery);
            XQResultSequence result = expr.executeQuery();
            return result.next();
        } catch (Exception e) {
            return false;
        }
    }

    // ANFÄLLIG: Datenextraktion
    public List<String> searchUsers(String searchTerm) {
        // ANFÄLLIG: Benutzereingabe in Abfrage
        String xquery =
            "for $user in doc('users.xml')//user " +
            "where contains($user/name, '" + searchTerm + "') " +
            "return $user/email/text()";

        // Angriff: searchTerm = "') or true() or contains($user/name, '"
        // Gibt alle E-Mails zurück

        return executeQuery(xquery);
    }

    // ANFÄLLIG: Dokumentzugriff
    public String getDocument(String docName) {
        // ANFÄLLIG: Dokumentname vom Benutzer
        String xquery = "doc('" + docName + "')";

        // Angriff: docName = "users.xml')//password | doc('public.xml"
        // Extrahiert Passwörter

        return executeQuery(xquery);
    }
}
# ANFÄLLIG: Python XQuery-Injection
from xmldb import XMLDatabase

class VulnerableXMLDatabase:
    def __init__(self):
        self.db = XMLDatabase()

    def login(self, username, password):
        # ANFÄLLIG: Direkte String-Formatierung
        xquery = f"""
            for $user in doc('users.xml')//user
            where $user/username = '{username}'
            and $user/password = '{password}'
            return $user
        """
        # Angriff: username = "' or '1'='1"

        result = self.db.execute(xquery)
        return len(result) > 0

    def search_products(self, category, min_price):
        # ANFÄLLIG: Mehrere Injektionspunkte
        xquery = f"""
            for $p in doc('products.xml')//product
            where $p/category = '{category}'
            and $p/price >= {min_price}
            return $p/name/text()
        """
        # Angriff: category = "electronics' or '1'='1' or 'x'='"
        # Angriff: min_price = "0 or true()"

        return self.db.execute(xquery)

    def get_user_data(self, user_id):
        # ANFÄLLIG: ID-Injection
        xquery = f"""
            doc('users.xml')//user[@id='{user_id}']/data
        """
        # Angriff: user_id = "1' or @id='2' or @id='"
        # Gibt Daten mehrerer Benutzer zurück

        return self.db.execute(xquery)
// ANFÄLLIG: Node.js XQuery-Injection
const xmldb = require('xmldb');

class VulnerableXQueryService {
    constructor() {
        this.db = new xmldb.Database();
    }

    async authenticate(username, password) {
        // ANFÄLLIG: Template-Literal mit Benutzereingabe
        const xquery = `
            for $user in doc('users.xml')//user
            where $user/username = '${username}'
            and $user/password = '${password}'
            return $user
        `;

        const result = await this.db.query(xquery);
        return result.length > 0;
    }

    async searchDocuments(keyword) {
        // ANFÄLLIG: Benutzereingabe in contains()
        const xquery = `
            for $doc in collection('documents')//document
            where contains($doc/content, '${keyword}')
            return $doc
        `;
        // Angriff: keyword = "') or true() or contains($doc/content, '"

        return await this.db.query(xquery);
    }

    async getConfiguration(configName) {
        // ANFÄLLIG: Konfigurationsname-Injection
        const xquery = `doc('config.xml')//config[@name='${configName}']`;
        // Angriff: configName = "debug'] | //user/password | //config[@name='x"

        return await this.db.query(xquery);
    }
}
(: ANFÄLLIG: XQuery innerhalb von XQuery :)

(: Dieses XQuery-Modul ist anfällig wenn Parameter von Benutzern kommen :)

declare function local:authenticate($username as xs:string, $password as xs:string) {
    (: ANFÄLLIG: Direkte Verwendung von Parametern in Abfrage :)
    let $query := concat(
        "for $user in doc('users.xml')//user ",
        "where $user/username = '", $username, "' ",
        "and $user/password = '", $password, "' ",
        "return $user"
    )
    return xquery:eval($query)
};

declare function local:search($term as xs:string) {
    (: ANFÄLLIG: Benutzereingabe in FLWOR :)
    for $item in doc('items.xml')//item
    where contains($item/name, $term)  (: Direkte Verwendung von $term ist anfällig :)
    return $item
};

Korrigierter Code

// SICHER: XQuery mit parametrisierten Ausdrücken
public class SafeXQueryAuth {

    private XQConnection connection;

    public boolean authenticate(String username, String password) {
        // SICHER: Verwende externe Variablen (parametrisierte Abfrage)
        String xquery =
            "declare variable $user external; " +
            "declare variable $pass external; " +
            "for $u in doc('users.xml')//user " +
            "where $u/username = $user " +
            "and $u/password = $pass " +
            "return $u";

        try {
            XQPreparedExpression expr = connection.prepareExpression(xquery);

            // Binde Parameter sicher
            expr.bindString(new QName("user"), username, null);
            expr.bindString(new QName("pass"), password, null);

            XQResultSequence result = expr.executeQuery();
            return result.next();
        } catch (Exception e) {
            return false;
        }
    }

    // SICHER: Validierte Eingabe
    public List<String> searchUsers(String searchTerm) {
        // Validiere Eingabe
        if (!isValidSearchTerm(searchTerm)) {
            throw new IllegalArgumentException("Ungültiger Suchbegriff");
        }

        String xquery =
            "declare variable $term external; " +
            "for $user in doc('users.xml')//user " +
            "where contains($user/name, $term) " +
            "return $user/email/text()";

        XQPreparedExpression expr = connection.prepareExpression(xquery);
        expr.bindString(new QName("term"), searchTerm, null);

        return executeQuery(expr);
    }

    private boolean isValidSearchTerm(String term) {
        // Whitelist alphanumerisch und Leerzeichen
        return term != null &&
               term.length() <= 100 &&
               term.matches("^[a-zA-Z0-9\\s]+$");
    }

    // SICHER: Whitelist für Dokumentnamen
    private static final Set<String> ALLOWED_DOCS = Set.of(
        "products.xml", "categories.xml", "public.xml"
    );

    public String getDocument(String docName) {
        // Validiere Dokumentname
        if (!ALLOWED_DOCS.contains(docName)) {
            throw new SecurityException("Dokument nicht erlaubt");
        }

        String xquery = "declare variable $doc external; doc($doc)";
        XQPreparedExpression expr = connection.prepareExpression(xquery);
        expr.bindString(new QName("doc"), docName, null);

        return executeQuery(expr);
    }
}
# SICHER: Python XQuery mit ordnungsgemäßem Escaping und Validierung
import re
from xmldb import XMLDatabase

class SafeXMLDatabase:
    def __init__(self):
        self.db = XMLDatabase()

    def escape_xquery_string(self, value):
        """Escape spezielle Zeichen für XQuery-Strings."""
        if value is None:
            return ''
        # Escape Anführungszeichen durch Verdopplung
        if "'" in value and '"' in value:
            # Verwende concat für Strings mit beiden Anführungstypen
            parts = value.split("'")
            return "concat('" + "', \"'\", '".join(parts) + "')"
        elif "'" in value:
            return '"' + value + '"'
        else:
            return "'" + value + "'"

    def validate_input(self, value, max_length=100):
        """Validiere Eingabe gegen Whitelist."""
        if not value or len(value) > max_length:
            return None
        if not re.match(r'^[a-zA-Z0-9_\- ]+$', value):
            return None
        return value

    def login(self, username, password):
        # Validiere Eingaben
        username = self.validate_input(username)
        password = self.validate_input(password)

        if not username or not password:
            return False

        # SICHER: Verwende externe Variablen
        xquery = """
            declare variable $user external;
            declare variable $pass external;
            for $u in doc('users.xml')//user
            where $u/username = $user
            and $u/password = $pass
            return $u
        """

        result = self.db.execute(xquery, {
            'user': username,
            'pass': password
        })
        return len(result) > 0

    def search_products(self, category, min_price):
        # Validiere Kategorie
        category = self.validate_input(category)
        if not category:
            raise ValueError("Ungültige Kategorie")

        # Validiere Preis als Zahl
        try:
            min_price = float(min_price)
            if min_price < 0:
                raise ValueError()
        except (ValueError, TypeError):
            raise ValueError("Ungültiger Preis")

        # SICHER: Parametrisierte Abfrage
        xquery = """
            declare variable $cat external;
            declare variable $price external;
            for $p in doc('products.xml')//product
            where $p/category = $cat
            and $p/price >= $price
            return $p/name/text()
        """

        return self.db.execute(xquery, {
            'cat': category,
            'price': min_price
        })

    # SICHER: Whitelist für Dokumentzugriff
    ALLOWED_DOCS = {'products.xml', 'categories.xml', 'public.xml'}

    def get_document(self, doc_name):
        if doc_name not in self.ALLOWED_DOCS:
            raise ValueError("Dokument nicht erlaubt")

        xquery = """
            declare variable $docname external;
            doc($docname)
        """

        return self.db.execute(xquery, {'docname': doc_name})
// SICHER: Node.js XQuery mit Parametrisierung
class SafeXQueryService {
    constructor(db) {
        this.db = db;
    }

    validateInput(value, maxLength = 100) {
        if (!value || typeof value !== 'string') {
            return null;
        }
        if (value.length > maxLength) {
            return null;
        }
        if (!/^[a-zA-Z0-9_\- ]+$/.test(value)) {
            return null;
        }
        return value;
    }

    async authenticate(username, password) {
        // Validiere Eingaben
        username = this.validateInput(username);
        password = this.validateInput(password);

        if (!username || !password) {
            return false;
        }

        // SICHER: Verwende parametrisierte Abfrage
        const xquery = `
            declare variable $user external;
            declare variable $pass external;
            for $u in doc('users.xml')//user
            where $u/username = $user
            and $u/password = $pass
            return $u
        `;

        const result = await this.db.query(xquery, {
            user: username,
            pass: password
        });

        return result.length > 0;
    }

    async searchDocuments(keyword) {
        // Validiere Schlüsselwort
        keyword = this.validateInput(keyword);
        if (!keyword) {
            throw new Error('Ungültiges Such-Schlüsselwort');
        }

        // SICHER: Parametrisierte Suche
        const xquery = `
            declare variable $term external;
            for $doc in collection('documents')//document
            where contains($doc/content, $term)
            return $doc
        `;

        return await this.db.query(xquery, { term: keyword });
    }

    // SICHER: Whitelist erlaubter Konfigurationen
    static ALLOWED_CONFIGS = new Set(['theme', 'language', 'timezone']);

    async getConfiguration(configName) {
        if (!SafeXQueryService.ALLOWED_CONFIGS.has(configName)) {
            throw new Error('Konfiguration nicht erlaubt');
        }

        const xquery = `
            declare variable $name external;
            doc('config.xml')//config[@name=$name]
        `;

        return await this.db.query(xquery, { name: configName });
    }
}
(: SICHER: XQuery mit ordnungsgemäßer Parameterbehandlung :)

declare function local:authenticate-safe(
    $username as xs:string,
    $password as xs:string
) as element()? {
    (: SICHER: Direkter Vergleich mit typisierten Parametern :)
    (: XQuery-Prozessor behandelt Escaping :)
    for $user in doc('users.xml')//user
    where $user/username = $username
    and $user/password = $password
    return $user
};

(: SICHER: Eingabevalidierungsfunktion :)
declare function local:validate-input($input as xs:string?) as xs:string? {
    if (empty($input)) then ()
    else if (string-length($input) > 100) then ()
    else if (not(matches($input, '^[a-zA-Z0-9_\- ]+$'))) then ()
    else $input
};

declare function local:search-safe($term as xs:string) as element()* {
    let $validated := local:validate-input($term)
    return
        if (empty($validated)) then ()
        else
            for $item in doc('items.xml')//item
            where contains($item/name, $validated)
            return $item
};

Ausgenutzt in der Praxis

Authentifizierungsumgehung

XQuery-Injection zur Umgehung XML-basierter Authentifizierung.

Datenextraktion

Extraktion sensibler Daten aus XML-Datenbanken.

LDAP/XML-Hybridangriffe

Kombinierte XQuery- und LDAP-Injection.


Tools zum Testen/Ausnutzen

  • XMLSpy - XQuery-Tests.

  • Benutzerdefinierte Injection-Skripte.

  • Burp Suite mit XQuery-Payloads.


CVE-Beispiele

  • CVEs in XML-Datenbankprodukten.

  • XQuery-Injection in Content-Management-Systemen.


Referenzen

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

  2. W3C. "XQuery Language Specification."