Review LDAP filters composed from strings
Matches methods that assign DirectorySearcher.Filter, construct a DirectorySearcher, a SearchRequest or a DirectoryEntry from a string, or call LdapConnection.Search(), while also assembling a string in the same method (String.Concat(), String.Format(), interpolation, StringBuilder).
LDAP filters have their own syntax: a value containing )(uid=) or \) closes the intended clause and opens another, which turns a login lookup into a query that matches every user, or bypasses the authentication altogether. This is LDAP injection, CWE-90 (OWASP Top 10 A03 Injection).
Flagged Code
LDAP filters have their own syntax: a value containing )(uid=) or \) closes the intended clause and opens another, which turns a login lookup into a query that matches every user, or bypasses the authentication altogether. This is LDAP injection, CWE-90 (OWASP Top 10 A03 Injection).
Flagged Code
static SearchResult FindUser(DirectoryEntry root, string login) {
var searcher = new DirectorySearcher(root);
searcher.Filter = "(&(objectClass=user)(sAMAccountName=" + login + "))"; // flagged
return searcher.FindOne();
}
Fixed Codestatic SearchResult FindUser(DirectoryEntry root, string login) {
var searcher = new DirectorySearcher(root);
searcher.Filter = LdapFilter.ForUser(login); // escaped and composed in one reviewed helper
return searcher.FindOne();
}
static class LdapFilter {
public static string ForUser(string login) =>
"(&(objectClass=user)(sAMAccountName=" + Escape(login) + "))";
static string Escape(string v) => // RFC 4515: escape \ * ( ) and NUL
v.Replace(@"\", @"\5c").Replace("*", @"\2a").Replace("(", @"\28")
.Replace(")", @"\29").Replace("\0", @"\00");
}How to fix
Escape every dynamic value inserted in a filter as RFC 4515 requires (the characters backslash, parenthesis, star and NUL become \5c, \28, \29, \2a, \00), and escape values inserted in a distinguished name as RFC 4514 requires. Validate the value against the expected shape first (a login is alphanumeric, an email has one @).
Keep the escaping and the composition in one dedicated helper, as in the fixed code: the method that assigns the filter no longer assembles strings itself, and the rule cannot tell an escaped fragment from a raw one.
Keep the escaping and the composition in one dedicated helper, as in the fixed code: the method that assigns the filter no longer assembles strings itself, and the rule cannot tell an escaped fragment from a raw one.
CQLinq source
This rule is written in CQLinq, C# LINQ over the NDepend code model. It ships with every new NDepend project: you can read it, adapt it, or use it as a starting point for your own custom rules.
// <Name>Review LDAP filters composed from strings</Name>
// <Id>ND3137:ReviewLdapFiltersComposedFromStrings</Id>
warnif count > 0
// Third-party members are part of the code model only when the application uses them.
// Members that take an LDAP filter or search base as a string.
let ldapFilterMembers = ThirdParty.Methods.Where(m =>
(m.ParentType.FullName == "System.DirectoryServices.DirectorySearcher" &&
(m.SimpleName == "set_Filter" || (m.IsConstructor && m.Name.Contains("String")))) ||
(m.ParentType.FullName == "System.DirectoryServices.Protocols.SearchRequest" &&
(m.SimpleName == "set_Filter" || (m.IsConstructor && m.Name.Contains("String")))) ||
(m.ParentType.FullName == "System.DirectoryServices.DirectoryEntry" && m.IsConstructor && m.Name.Contains("String")) ||
(m.ParentType.FullName == "Novell.Directory.Ldap.LdapConnection" && m.SimpleName.StartsWith("Search")))
.ToHashSetEx()
where ldapFilterMembers.Any()
// Concatenation, formatting, interpolation or a builder: the filter is assembled in the method.
let stringBuildingMembers = ThirdParty.Methods.Where(m =>
(m.ParentType.FullName == "System.String" && m.SimpleNameEqualsAny("Concat", "Format", "Join", "Replace", "Insert")) ||
m.ParentType.FullNameEqualsAny("System.Text.StringBuilder", "System.Runtime.CompilerServices.DefaultInterpolatedStringHandler"))
.ToHashSetEx()
from m in Application.Methods.UsingAny(ldapFilterMembers)
where m.MethodsCalled.Intersect(stringBuildingMembers).Any()
select new {
m,
ldapFilterMembersUsed = m.MethodsCalled.Intersect(ldapFilterMembers).ToArray(),
Debt = 15.ToMinutes().ToDebt(),
Severity = Severity.High
}
//<Expl>
// The method *{0}* assembles a string and passes it to *{1}* as an LDAP
// filter or path. If any fragment derives from user input, characters
// such as *)(|* rewrite the filter: use an escaping helper on every
// dynamic value.
//</Expl>
// <Desc>
// Matches methods that assign *DirectorySearcher.Filter*, construct a
// *DirectorySearcher*, a *SearchRequest* or a *DirectoryEntry* from a
// string, or call *LdapConnection.Search()*, while also assembling a
// string in the same method (*String.Concat()*, *String.Format()*,
// interpolation, *StringBuilder*).
//
// LDAP filters have their own syntax: a value containing *)(uid=*)* or
// *\*)* closes the intended clause and opens another, which turns a
// login lookup into a query that matches every user, or bypasses the
// authentication altogether. This is **LDAP injection**, CWE-90 (OWASP
// Top 10 A03 Injection).
//
// **Flagged Code**
// ```csharp
// static SearchResult FindUser(DirectoryEntry root, string login) {
// var searcher = new DirectorySearcher(root);
// searcher.Filter = "(&(objectClass=user)(sAMAccountName=" + login + "))"; // flagged
// return searcher.FindOne();
// }
// ```
//
// **Fixed Code**
// ```csharp
// static SearchResult FindUser(DirectoryEntry root, string login) {
// var searcher = new DirectorySearcher(root);
// searcher.Filter = LdapFilter.ForUser(login); // escaped and composed in one reviewed helper
// return searcher.FindOne();
// }
// static class LdapFilter {
// public static string ForUser(string login) =>
// "(&(objectClass=user)(sAMAccountName=" + Escape(login) + "))";
// static string Escape(string v) => // RFC 4515: escape \ * ( ) and NUL
// v.Replace(@"\", @"\5c").Replace("*", @"\2a").Replace("(", @"\28")
// .Replace(")", @"\29").Replace("\0", @"\00");
// }
// ```
// </Desc>
// <HowToFix>
// Escape every dynamic value inserted in a filter as RFC 4515 requires
// (the characters backslash, parenthesis, star and NUL become *\5c*,
// *\28*, *\29*, *\2a*, *\00*), and escape values inserted in a
// distinguished name as RFC 4514 requires. Validate the value against
// the expected shape first (a login is alphanumeric, an email has one
// *@*).
//
// Keep the escaping and the composition in one dedicated helper, as in
// the fixed code: the method that assigns the filter no longer
// assembles strings itself, and the rule cannot tell an escaped fragment
// from a raw one.
// </HowToFix>
Open in the interactive Rules Explorer · All default rules
Security: all 39 items
- Software Composition Analysis (SCA) ND3120
- Transitive Software Composition Analysis (SCA) ND3121
- Don't hard-code secrets in constants ND3114
- Review credentials built from hard-coded values ND3115
- Don't use insecure deserializers ND3110
- Don't use weak hash algorithms (MD5, SHA1) ND3109
- Don't use DES/3DES/RC2 weak cipher algorithms ND3102
- Don't use weak password-based key derivation ND3123
- Don't use the ECB encryption mode ND3132
- Don't disable certificate validation ND3103
- Don't hard-code TLS/SSL protocol versions ND3122
- Review commands vulnerable to SQL injection ND3107
- Review data adapters vulnerable to SQL injection ND3108
- Review raw SQL built from composed strings ND3128
- Review LDAP filters composed from strings ND3137
- Don't enable external XML entity resolution (XXE) ND3111
- Review processes started with a composed arguments string ND3124
- Review web endpoints building file paths from their parameters ND3129
- Review custom archive extraction (Zip Slip) ND3135
- Review assemblies and types loaded from runtime-provided names ND3125
- Review web endpoints without [Authorize] nor [AllowAnonymous] ND3118
- Review state-changing endpoints opened to anonymous callers ND3130
- Review state-changing MVC actions without anti-forgery validation ND3119
- Review HTML-encoding bypasses ND3133
- Review explicitly disabled request validation ND3134
- Review redirects to a URL that is not validated as local ND3126
- Review request-influenced outbound HTTP calls (SSRF) ND3136
- Review insecure ASP.NET Core CORS policies and unguarded developer exception page ND3116
- Review cookies created without HttpOnly and Secure flags ND3117
- Review regular expressions without a match timeout (ReDoS) ND3112
- Review secrets compared with non-constant-time equality ND3113
- Review secrets passed to logging methods ND3127
- Review security operations that log nothing ND3131
- Don't use System.Random for security purposes ND3101
- Don't use CoSetProxyBlanket and CoInitializeSecurity ND3100
- Review publicly visible event handlers ND3104
- Pointers should not be publicly visible ND3105
- Seal methods that satisfy non-public interfaces ND3106
- Restrict sensitive APIs to reviewed components ND3138