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
| Auswirkung | Details |
|---|---|
| Vertraulichkeit | Umfang: Datenoffenlegung Nicht autorisierter Zugriff auf XML-Daten. |
| Authentifizierung | Umfang: Umgehung Authentifizierungslogik umgangen. |
| Integrität | Umfang: 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
-
MITRE. "CWE-652: Improper Neutralization of Data within XQuery Expressions." https://cwe.mitre.org/data/definitions/652.html
-
W3C. "XQuery Language Specification."