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:

JavaScript ldap-vulnerable.js
// 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:

Plain ldap-injected-filter.txt
(&(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:

Plain ldap-auth-bypass.txt
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:

ZeichenBedeutungEscape
(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):

JavaScript ldap-secure-node.js
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):

Java ldap-secure-java.java
// 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):

Python ldap-secure-python.py
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:

  1. Search für den User (mit escaped Username) → userDN.
  2. Bind auf userDN mit 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:

Java xpath-vulnerable.java
// 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":

XPath xpath-injected.txt
/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):

Java xpath-variables-java.java
// 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 konkateniert

Lö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():

Java xpath-concat-escape.java
// 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):

Python xpath-secure-python.py
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:

Plain blind-xpath-extraction.txt
# 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 erratbar

XPath 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:

Plain ldap-test-payloads.txt
# Filter-Manipulation
*
*)(&)
admin)(&)
admin)(|(uid=*

# Wildcard-Brute
a*
ad*
adm*

# Klammer-Mismatch (DoS oder Error)
((
))

XPath-Test-Payloads:

Plain xpath-test-payloads.txt
# 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='1

Detection:

  • 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.evaluate mit 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

/ Weiter

Zurück zu Injection (SQL/NoSQL/Cmd)

Zur Übersicht