Add escaping for fields with wildcard search

Closes #20510
This commit is contained in:
Alexander Schwartz 2023-05-23 17:29:08 +02:00 committed by Hynek Mlnařík
parent a29c30ccd5
commit 512e30b210
4 changed files with 40 additions and 17 deletions

View file

@ -431,16 +431,16 @@ public class LDAPStorageProvider implements UserStorageProvider,
// Mapper should replace parameter with correct LDAP mapped attributes
if (attributes.containsKey(UserModel.USERNAME)) {
ldapQuery.addWhereCondition(conditionsBuilder.equal(UserModel.USERNAME, attributes.get(UserModel.USERNAME), EscapeStrategy.NON_ASCII_CHARS_ONLY));
ldapQuery.addWhereCondition(conditionsBuilder.equal(UserModel.USERNAME, attributes.get(UserModel.USERNAME), EscapeStrategy.DEFAULT_EXCEPT_ASTERISK));
}
if (attributes.containsKey(UserModel.EMAIL)) {
ldapQuery.addWhereCondition(conditionsBuilder.equal(UserModel.EMAIL, attributes.get(UserModel.EMAIL), EscapeStrategy.NON_ASCII_CHARS_ONLY));
ldapQuery.addWhereCondition(conditionsBuilder.equal(UserModel.EMAIL, attributes.get(UserModel.EMAIL), EscapeStrategy.DEFAULT_EXCEPT_ASTERISK));
}
if (attributes.containsKey(UserModel.FIRST_NAME)) {
ldapQuery.addWhereCondition(conditionsBuilder.equal(UserModel.FIRST_NAME, attributes.get(UserModel.FIRST_NAME), EscapeStrategy.NON_ASCII_CHARS_ONLY));
ldapQuery.addWhereCondition(conditionsBuilder.equal(UserModel.FIRST_NAME, attributes.get(UserModel.FIRST_NAME), EscapeStrategy.DEFAULT_EXCEPT_ASTERISK));
}
if (attributes.containsKey(UserModel.LAST_NAME)) {
ldapQuery.addWhereCondition(conditionsBuilder.equal(UserModel.LAST_NAME, attributes.get(UserModel.LAST_NAME), EscapeStrategy.NON_ASCII_CHARS_ONLY));
ldapQuery.addWhereCondition(conditionsBuilder.equal(UserModel.LAST_NAME, attributes.get(UserModel.LAST_NAME), EscapeStrategy.DEFAULT_EXCEPT_ASTERISK));
}
// for all other searchable fields: Ignoring is the fallback option, since it may overestimate the results but does not ignore matches.
// for empty params: all users are returned (pagination applies)
@ -476,10 +476,10 @@ public class LDAPStorageProvider implements UserStorageProvider,
s += "*";
}
conditions.add(conditionsBuilder.equal(UserModel.USERNAME, s.trim().toLowerCase(), EscapeStrategy.NON_ASCII_CHARS_ONLY));
conditions.add(conditionsBuilder.equal(UserModel.EMAIL, s.trim().toLowerCase(), EscapeStrategy.NON_ASCII_CHARS_ONLY));
conditions.add(conditionsBuilder.equal(UserModel.FIRST_NAME, s, EscapeStrategy.NON_ASCII_CHARS_ONLY));
conditions.add(conditionsBuilder.equal(UserModel.LAST_NAME, s, EscapeStrategy.NON_ASCII_CHARS_ONLY));
conditions.add(conditionsBuilder.equal(UserModel.USERNAME, s.trim().toLowerCase(), EscapeStrategy.DEFAULT_EXCEPT_ASTERISK));
conditions.add(conditionsBuilder.equal(UserModel.EMAIL, s.trim().toLowerCase(), EscapeStrategy.DEFAULT_EXCEPT_ASTERISK));
conditions.add(conditionsBuilder.equal(UserModel.FIRST_NAME, s, EscapeStrategy.DEFAULT_EXCEPT_ASTERISK));
conditions.add(conditionsBuilder.equal(UserModel.LAST_NAME, s, EscapeStrategy.DEFAULT_EXCEPT_ASTERISK));
ldapQuery.addWhereCondition(conditionsBuilder.orCondition(conditions.toArray(Condition[]::new)));
}

View file

@ -24,17 +24,35 @@ import java.nio.charset.StandardCharsets;
*/
public enum EscapeStrategy {
// LDAP special characters like * ( ) \ are not escaped. Only non-ASCII characters like é are escaped
NON_ASCII_CHARS_ONLY {
/**
* LDAP special character * is not escaped, other special characters are escaped. Non-ASCII characters like é are escaped.
* Use it for searches where wildcards are allowed.
*/
DEFAULT_EXCEPT_ASTERISK {
@Override
public String escape(String input) {
StringBuilder output = new StringBuilder();
for (byte b : input.getBytes(StandardCharsets.UTF_8)) {
switch (b) {
case 0x5c:
output.append("\\5c"); // \
break;
case 0x28:
output.append("\\28"); // (
break;
case 0x29:
output.append("\\29"); // )
break;
case 0x00:
output.append("\\00"); // \u0000
break;
default: {
appendByte(b, output);
}
}
}
return output.toString();
}
@ -42,7 +60,9 @@ public enum EscapeStrategy {
},
// Escaping of LDAP special characters including non-ASCII characters like é
/**
* Escaping of LDAP special characters including non-ASCII characters like é.
*/
DEFAULT {

View file

@ -27,16 +27,16 @@ import org.keycloak.storage.ldap.idm.query.EscapeStrategy;
public class EscapeTest {
@Test
public void testNoAsciiOnlyEscaping() throws Exception {
public void testEscapingExceptAsterisk() {
String text = "Véronique* Martin(john)second\\fff//eee\u0000";
Assert.assertEquals(EscapeStrategy.NON_ASCII_CHARS_ONLY.escape(text), "V\\c3\\a9ronique* Martin(john)second\\fff//eee\u0000");
Assert.assertEquals(EscapeStrategy.DEFAULT_EXCEPT_ASTERISK.escape(text), "V\\c3\\a9ronique* Martin\\28john\\29second\\5cfff//eee\\00");
text = "Hi This is a test #çà";
Assert.assertEquals(EscapeStrategy.DEFAULT.escape(text), "Hi This is a test #\\c3\\a7\\c3\\a0");
Assert.assertEquals(EscapeStrategy.DEFAULT_EXCEPT_ASTERISK.escape(text), "Hi This is a test #\\c3\\a7\\c3\\a0");
}
@Test
public void testEscaping() throws Exception {
public void testEscaping() {
String text = "Véronique* Martin(john)second\\fff//eee\u0000";
Assert.assertEquals(EscapeStrategy.DEFAULT.escape(text), "V\\c3\\a9ronique\\2a Martin\\28john\\29second\\5cfff//eee\\00");

View file

@ -1023,6 +1023,9 @@ public class LDAPProvidersIntegrationTest extends AbstractLDAPTest {
// search by a string that matches multiple fields. Should still return the one entity it matches.
Assert.assertEquals(1, session.users().searchForUserStream(appRealm, "*11*").count());
LDAPTestAsserts.assertUserImported(UserStoragePrivateUtil.userLocalStorage(session), appRealm, "username11", "John11", "Doel11", "user11@email.org", "124");
// search by a string that has special characters. Should succeed with an empty set, but no exceptions.
Assert.assertEquals(0, session.users().searchForUserStream(appRealm, "John)").count());
});
}