LDAP- und XPath-Injection sind die weniger bekannten Geschwister von SQL-Injection — gleiche Klasse, andere Interpreter. LDAP-Verzeichnisse (Active Directory, OpenLDAP) und XML-Datenquellen tauchen in vielen Enterprise-Apps auf, oft als Auth-Backend. Wer Benutzer-Eingaben ungeschützt in den Suchfilter einbettet, baut Auth-Bypass mit denselben Mustern wie OR 1=1. Dieser Artikel zeigt beide Sprachen, ihre Sonderzeichen und die Escape-APIs, die strukturell schützen.
LDAP-Injection — wie sie entsteht
LDAP-Suchfilter sind eine eigene kleine Query-Sprache mit Klammer-basierter Syntax: (attribute=value). Boolesche Verknüpfungen mit &, |, !. Wildcards mit *.
Klassischer Login-Code mit LDAP:
// Schadhaft
const filter = `(&(uid=${username})(userPassword=${password}))`;
ldapClient.search('ou=users,dc=example,dc=com', { filter }, callback);Mit username = "*" und password = "*" wird der Filter zu:
(&(uid=*)(userPassword=*))Das matched alle Einträge — der Server liefert mindestens einen Treffer zurück, App-Code interpretiert das als „Login erfolgreich". Auth-Bypass ohne Credentials.
Klassischer Auth-Bypass mit Filter-Manipulation:
username = admin)(&)
password = irgendwas
Resulting filter:
(&(uid=admin)(&))(userPassword=irgendwas))
Effektiv evaluiert: (&(uid=admin)(&)) — passwort wird ignoriert(&) ist ein leerer AND-Block, der immer wahr ist. Der zweite Teil (userPassword=irgendwas)) wird vom Server als Anhang ignoriert. Admin-Login ohne Passwort.
LDAP-Special-Characters
Die LDAP-Filter-Syntax (RFC 4515) reserviert folgende Zeichen mit Sonderbedeutung:
| Zeichen | Bedeutung | Escape |
|---|---|---|
( | Filter-Klammer öffnen | \28 |
) | Filter-Klammer schließen | \29 |
* | Wildcard | \2a |
\ | Escape-Zeichen | \5c |
NUL (\0) | String-Terminator | \00 |
Wer diese Zeichen ungeschützt in einen Filter einbettet, lässt User-Input die Filter-Struktur ändern. Anders als SQL hat LDAP kein direktes Prepared-Statement-Konzept — die Verteidigung liegt im Escapen der Werte.
LDAP-Injection sicher umgehen
Node.js (ldapjs):
const ldap = require('ldapjs');
// Manuelle Escape-Funktion (ldapjs hat keine eingebaute)
function escapeLDAP(value) {
return String(value).replace(/[\\*()�]/g, c => {
return '\\' + c.charCodeAt(0).toString(16).padStart(2, '0');
});
}
const filter = `(&(uid=${escapeLDAP(username)})(userPassword=${escapeLDAP(password)}))`;
ldapClient.search(baseDN, { filter }, callback);Java (mit JNDI):
// OWASP ESAPI bietet encodeForLDAP
import org.owasp.esapi.ESAPI;
String safeUser = ESAPI.encoder().encodeForLDAP(username);
String safePass = ESAPI.encoder().encodeForLDAP(password);
String filter = "(&(uid=" + safeUser + ")(userPassword=" + safePass + "))";
SearchControls sc = new SearchControls();
sc.setSearchScope(SearchControls.SUBTREE_SCOPE);
NamingEnumeration<SearchResult> results = ctx.search(baseDN, filter, sc);Python (ldap3):
from ldap3.utils.conv import escape_filter_chars
safe_user = escape_filter_chars(username)
safe_pass = escape_filter_chars(password)
search_filter = f"(&(uid={safe_user})(userPassword={safe_pass}))"
conn.search(search_base=base_dn, search_filter=search_filter)Universelles Pattern: vor jeder Einbettung von User-Input in einen LDAP-Filter eine Escape-Funktion anwenden. Eine einzige nicht-escapete Stelle reicht.
Besser noch — Auth-Architektur trennen:
Statt LDAP-Filter mit Passwort zu bauen (suchen + Passwort vergleichen in einem Schritt), die zwei Phasen explizit trennen:
- Search für den User (mit escaped Username) →
userDN. - Bind auf
userDNmit dem Passwort als Bind-Credential.
Die LDAP-Bind-Operation ist die autoritative Auth-Methode des Servers — das Passwort wird nicht als Filter-Teil interpretiert, sondern als Bind-Credential validiert. Damit ist Passwort-basierte LDAP-Injection strukturell unmöglich.
XPath-Injection — die XML-Variante
XPath ist die Query-Sprache für XML-Dokumente. Apps, die Daten in XML halten (Legacy-Konfigurationen, SOAP-Services, manche CMS-Backends), nutzen XPath-Queries — und sind damit für die gleiche Klasse anfällig.
Klassisches Login-Beispiel:
// Schadhaft
String expr = "/users/user[name='" + username + "' and password='" + password + "']";
XPath xpath = XPathFactory.newInstance().newXPath();
NodeList result = (NodeList) xpath.evaluate(expr, xmlDoc, XPathConstants.NODESET);Mit username = "admin" und password = "x' or '1'='1":
/users/user[name='admin' and password='x' or '1'='1']Die Tautologie '1'='1' macht das Prädikat immer wahr — alle User-Nodes matchen — Auth-Bypass.
XPath-Injection sicher umgehen
XPath 1.0 hat kein eingebautes Prepared-Statement-Konzept. XPath 2.0+ unterstützt Variablen-Bindings — die meisten Anwendungen nutzen aber 1.0 (Default in JDK).
Lösung 1: Variable-Bindings (XPath 2.0 oder JDK mit Variable-Resolver):
// Variable-Resolver registrieren
XPath xpath = XPathFactory.newInstance().newXPath();
xpath.setXPathVariableResolver(qName -> {
switch (qName.getLocalPart()) {
case "username": return username;
case "password": return password;
default: return null;
}
});
String expr = "/users/user[name=$username and password=$password]";
NodeList result = (NodeList) xpath.evaluate(expr, xmlDoc, XPathConstants.NODESET);
// username und password werden als Werte gebunden, nicht in den Query-String konkateniertLösung 2: Manuelles Escapen (wenn Variable-Bindings nicht verfügbar):
XPath-String-Literals nutzen ' oder " als Delimiter. Wenn der Wert beide Zeichen enthält, hilft die XPath-Funktion concat():
// Hilfsfunktion: erzeugt sicheren XPath-String-Literal
public static String xpathLiteral(String value) {
if (!value.contains("'")) return "'" + value + "'";
if (!value.contains("\"")) return "\"" + value + "\"";
// Beide vorhanden — concat() zerlegt am Apostroph
StringBuilder sb = new StringBuilder("concat(");
String[] parts = value.split("'");
for (int i = 0; i < parts.length; i++) {
sb.append("'").append(parts[i]).append("'");
if (i < parts.length - 1) sb.append(", \"'\", ");
}
sb.append(")");
return sb.toString();
}
String expr = "/users/user[name=" + xpathLiteral(username)
+ " and password=" + xpathLiteral(password) + "]";Universell empfohlen ist Variante 1 (Variable-Bindings) — Variante 2 nur als Fallback.
Python (lxml):
from lxml import etree
tree = etree.parse('users.xml')
# XPath-Variable-Binding mit lxml
result = tree.xpath(
"/users/user[name=$user and password=$pw]",
user=username,
pw=password
)lxml hat Variable-Bindings als Standard-API — die einfachste Variante in der Python-Welt.
Daten-Extraktion mit Blind-XPath
Wie bei Blind-SQL-Injection lässt sich auch über XPath-Injection schrittweise extrahieren — selbst wenn die App nur „Login OK / Login Fail" zurückgibt.
Beispiel-Payload-Reihe:
# Wie viele User existieren?
username = ' or count(/users/user)=1 or '
username = ' or count(/users/user)=2 or '
...
# Erster Buchstabe des ersten Usernamens?
username = ' or substring((/users/user[1]/name)/text(),1,1)='a' or '
username = ' or substring((/users/user[1]/name)/text(),1,1)='b' or '
...
# Komplettes XML rekursiv erratbarXPath bietet sogar mehr Introspection-Funktionen als SQL (name(), local-name(), count(), position()), was Extraktion oft schneller macht.
Tool: xcat automatisiert Blind-XPath-Extraktion analog zu sqlmap.
Reale Vorfälle und Bug-Bounty-Funde
LDAP-Injection:
- Drupal-Modul-CVEs in LDAP-Auth-Plugins — wiederholt String-Konkat-Bugs in Filter-Aufbau.
- Apache Directory Studio, phpLDAPadmin — historische Lücken in Such-Eingaben.
- Microsoft Active Directory Federation Services (ADFS) — mehrere CVEs in Filter-Verarbeitung.
- Atlassian Jira/Confluence — LDAP-Auth-Plugins mit Injection-Bugs (2019, 2021).
XPath-Injection:
- IBM WebSphere — XPath-Injection in Konfigurations-Schnittstelle.
- Apache Cocoon — XPath-Eval mit unsicherer Eingabe.
- SOAP-Backends vieler Enterprise-Stacks — XML-Body wird per XPath ausgewertet, ohne Variable-Bindings.
Beide Klassen tauchen in Bug-Bounty-Reports regelmäßig auf, oft bei großen Enterprise-Targets mit Legacy-Stacks. Die Auszahlung ist häufig hoch (Auth-Bypass-Klasse), weil der Impact direkt ist.
Test-Strategien
LDAP-Test-Payloads:
# Filter-Manipulation
*
*)(&)
admin)(&)
admin)(|(uid=*
# Wildcard-Brute
a*
ad*
adm*
# Klammer-Mismatch (DoS oder Error)
((
))XPath-Test-Payloads:
# Tautologie
' or '1'='1
' or 1=1 or '
" or "1"="1
# Substring-Probing
' or substring(/users/user[1]/name,1,1)='a' or '
# Komplette Node-Liste
' or 1=1] | //*[1='1Detection:
- Unterschiedliche Response-Längen zwischen Tautologie und Nicht-Tautologie.
- Time-Based mit XPath-Funktionen, die teuer sind (
count(//*)auf großen Dokumenten). - Error-Based — viele XPath-Engines werfen sprechende Fehler bei Syntax-Bruch.
Statische Analyse:
- Semgrep-Regeln für
ctx.search,xpath.evaluatemit Variablen. - CodeQL Security-Pack — erkennt User-Input-zu-LDAP-Filter und User-Input-zu-XPath-Pfade.
- Code-Review nach String-Konkat in LDAP-/XPath-Aufrufen.
Besonderheiten
Distinguished Name (DN) vs. Filter — zwei Escape-Klassen
LDAP hat zwei Stellen, an denen User-Input auftaucht: Filter (RFC 4515) und DN-String (RFC 4514). Beide haben unterschiedliche Sonderzeichen und Escape-Regeln. Für DNs sind ,, =, +, <, >, #, ; und führende/trailing Whitespace problematisch. Wer User-Input in einen DN einbettet, muss eine zweite Escape-Funktion nutzen.
LDAP-Bind statt Filter-mit-Passwort
Die saubere LDAP-Auth-Architektur trennt die Suche (mit Service-Account) vom Bind (mit User-Credentials). Die Suche liefert die userDN, der nachfolgende Bind-Versuch mit dem User-Passwort validiert die Identität strukturell — kein Filter-basierter Passwort-Vergleich nötig. Praktisch alle modernen LDAP-Auth-Libraries machen das so; eigener Code sollte dem folgen.
XPath 1.0 vs. 2.0 — Variable-Support unterschiedlich
Java-Standard-XPath ist 1.0. Variable-Bindings werden über einen XPathVariableResolver implementiert — funktioniert, aber muss aktiv registriert werden. XPath 2.0 und 3.0 haben Variable-Deklarationen nativ. Saxon ist eine populäre XPath-2.0-Engine für JVM. lxml in Python und XOM in Java unterstützen Variable-Bindings als Standard-API.
XQuery-Injection — der größere Bruder
XPath ist eine Teilmenge von XQuery. Apps, die XQuery nutzen (BaseX, eXist-db, MarkLogic), haben dieselbe Klasse mit größerem Wirkungs-Raum — XQuery hat Funktionen für Datei-Zugriff, HTTP, Modul-Imports. XQuery-Injection kann zu RCE führen, nicht nur Datenextraktion.
OWASP ESAPI ist nicht mehr aktiv weiterentwickelt
OWASP ESAPI (Enterprise Security API) bietet Encoder für viele Sprachen, inkl. LDAP und XPath. Das Java-Projekt ist allerdings nicht mehr sehr aktiv — viele neuere Apps nutzen direkt die Library-eigenen Escape-Funktionen (ldap3, ldapjs) oder eigene Helper. ESAPI für neue Projekte ist Vorsicht geboten; bestehende Nutzung ok.
Active Directory-spezifische Eigenheiten
AD hat eigene Sonderheiten: NULL-Bytes in Werten (RFC-konform escapen), Wildcard-Suchen über sehr große Verzeichnisse können DoS-artige Effekte haben, und manche AD-Server haben Default-Limits auf Filter-Komplexität, die Injection-Versuche partiell abfangen. Verlass dich nicht darauf — das ist Server-Konfiguration, kein App-Schutz.
Web Service Backends sind oft das XML-Ziel
XPath-Injection findet sich heute selten in klassischen Web-Apps, häufig aber in SOAP-Backends, Java-EE-Services und älteren ESB-Setups. Wer eine moderne JSON-API nach hinten an einen Legacy-SOAP-Service routet, schleift die Klasse mit. Pentests auf solche Architekturen lohnen sich.
Weiterführende Ressourcen
Externe Quellen
- OWASP LDAP Injection Prevention Cheat Sheet
- OWASP XPath Injection Prevention Cheat Sheet
- PortSwigger Web Security Academy
- RFC 4515 — LDAP String Search Filter
- RFC 4514 — LDAP Distinguished Names
- xcat — Blind-XPath-Extraction-Tool
- ldap3 (Python) Docs
- lxml XPath Docs