Back to scan results
What this check probes
The discovery stage inspects same-origin GET forms and links for parameters such as username, user, uid, name, mail, search, query, filter, group, and dn. A bounded root-page fallback covers common names when no suitable public form is advertised.
Each selected input receives unmatched parentheses, a trailing LDAP escape, an invalid hexadecimal escape, adjacent invalid filter delimiters, and an invalid distinguished-name component. Responses are compared with a harmless baseline for .NET Directory Services, Java JNDI, OpenLDAP/PHP, Python LDAP, filter-parser, and DN-syntax errors.
A new LDAP-specific exception or a new HTTP 5xx response produces a warning. Ordinary HTTP 200 responses, reflection, response size, redirects, and generic validation messages are not treated as proof of injection.
The check never uses *, Boolean filter operators, tautologies, password attributes, login POSTs, guessed credentials, session cookies, directory enumeration, writes, or data-extraction payloads. Authentication forms, authenticated searches, JSON bodies, APIs not linked from the public page, and application-only directory calls remain outside this scan.
Why this matters for PCI DSS
LDAP injection can alter directory filters or distinguished names, potentially bypassing authentication, exposing employee or customer records, changing authorization decisions, or revealing Active Directory structure.
PCI DSS secure-development requirements expect applications to prevent injection, validate untrusted data, protect authentication systems, and limit service-account privileges. A pass here covers only the public GET surface tested with non-broadening syntax.
How to fix it
Never concatenate an unescaped value into an LDAP filter. .NET Framework's System.DirectoryServices does not provide SQL-style filter parameters, so escape filter values according to RFC 4515:
private static string EscapeLdapFilterValue(string value)
{
var output = new StringBuilder();
foreach (char c in value ?? "")
{
switch (c)
{
case '\\': output.Append(@"\5c"); break;
case '*': output.Append(@"\2a"); break;
case '(': output.Append(@"\28"); break;
case ')': output.Append(@"\29"); break;
case '\0': output.Append(@"\00"); break;
default: output.Append(c); break;
}
}
return output.ToString();
}
string account = EscapeLdapFilterValue(username);
using (var searcher = new DirectorySearcher(directoryRoot))
{
searcher.Filter = "(&(objectClass=user)(sAMAccountName=" + account + "))";
searcher.SizeLimit = 1;
searcher.ServerTimeLimit = TimeSpan.FromSeconds(3);
SearchResult result = searcher.FindOne();
}
LDAP filter escaping and distinguished-name escaping are different operations. Do not reuse this function when constructing a DN. Prefer a directory library's DN builder, or map a validated opaque identifier to a server-owned DN instead of accepting DN components from the client.
Apply a strict length and character allowlist appropriate to the field before escaping. Keep the filter structure, attribute names, requested attributes, search base, and scope fixed on the server. Never allow the client to supply raw filters, attribute names, object classes, search bases, or LDAP URLs.
Use a dedicated read-only bind account with the narrowest search base and attributes, enforce size and time limits, use LDAPS or StartTLS with certificate validation, and return generic errors. Authentication should fail closed and should not reveal whether the account, directory, filter, or bind step failed.