2020import static java .util .Objects .requireNonNull ;
2121
2222import java .time .Duration ;
23- import java .util .regex .Pattern ;
2423
2524import javax .naming .AuthenticationException ;
2625import javax .naming .NamingEnumeration ;
5857 */
5958public class SearchFirstActiveDirectoryRealm extends ActiveDirectoryRealm {
6059
61- private static final Pattern USERNAME_PLACEHOLDER = Pattern . compile ( " \\ {0}") ;
60+ private static final String USERNAME_PLACEHOLDER = " {0}" ;
6261 private static final String DEFAULT_SEARCH_FILTER = "cn={0}" ;
6362 private static final int DEFAULT_SEARCH_TIMEOUT_MILLIS = (int ) Duration .ofSeconds (10 ).toMillis ();
6463
65- @ Nullable
6664 private String searchFilter = DEFAULT_SEARCH_FILTER ;
6765 private int searchTimeoutMillis = DEFAULT_SEARCH_TIMEOUT_MILLIS ;
6866
6967 /**
7068 * Returns a search filter string.
7169 */
72- @ Nullable
7370 protected String getSearchFilter () {
7471 return searchFilter ;
7572 }
@@ -78,7 +75,10 @@ protected String getSearchFilter() {
7875 * Sets a search filter string.
7976 */
8077 protected void setSearchFilter (String searchFilter ) {
81- this .searchFilter = requireNonNull (searchFilter , "searchFilter" );
78+ requireNonNull (searchFilter , "searchFilter" );
79+ checkArgument (searchFilter .contains ("{0}" ),
80+ "searchFilter must contain '{0}' placeholder: %s" , searchFilter );
81+ this .searchFilter = searchFilter ;
8282 }
8383
8484 /**
@@ -157,11 +157,9 @@ protected String findUserDn(LdapContextFactory ldapContextFactory, String userna
157157 ctrl .setSearchScope (SearchControls .SUBTREE_SCOPE );
158158 ctrl .setTimeLimit (searchTimeoutMillis );
159159
160- final String filter =
161- searchFilter != null ? USERNAME_PLACEHOLDER .matcher (searchFilter )
162- .replaceAll (username )
163- : username ;
164- final NamingEnumeration <SearchResult > result = ctx .search (searchBase , filter , ctrl );
160+ final String escaped = encodeLdapFilter (username );
161+ final String filtered = searchFilter .replace (USERNAME_PLACEHOLDER , escaped );
162+ final NamingEnumeration <SearchResult > result = ctx .search (searchBase , filtered , ctrl );
165163 try {
166164 if (!result .hasMore ()) {
167165 return null ;
@@ -175,6 +173,40 @@ protected String findUserDn(LdapContextFactory ldapContextFactory, String userna
175173 }
176174 }
177175
176+ /**
177+ * Escapes special characters in the given value for use in an LDAP search filter,
178+ * as specified in <a href="https://datatracker.ietf.org/doc/html/rfc4515#section-3">RFC 4515 §3</a>.
179+ */
180+ static String encodeLdapFilter (String value ) {
181+ if (value == null ) {
182+ return "" ;
183+ }
184+ final StringBuilder sb = new StringBuilder (value .length ());
185+ for (int i = 0 ; i < value .length (); i ++) {
186+ final char c = value .charAt (i );
187+ switch (c ) {
188+ case '\\' :
189+ sb .append ("\\ 5c" );
190+ break ;
191+ case '*' :
192+ sb .append ("\\ 2a" );
193+ break ;
194+ case '(' :
195+ sb .append ("\\ 28" );
196+ break ;
197+ case ')' :
198+ sb .append ("\\ 29" );
199+ break ;
200+ case '\0' :
201+ sb .append ("\\ 00" );
202+ break ;
203+ default :
204+ sb .append (c );
205+ }
206+ }
207+ return sb .toString ();
208+ }
209+
178210 private static UsernamePasswordToken ensureUsernamePasswordToken (AuthenticationToken token ) {
179211 if (token instanceof UsernamePasswordToken ) {
180212 return (UsernamePasswordToken ) token ;
0 commit comments