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 // Mapper should replace parameter with correct LDAP mapped attributes
if (attributes.containsKey(UserModel.USERNAME)) { 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)) { 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)) { 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)) { 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 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) // for empty params: all users are returned (pagination applies)
@ -476,10 +476,10 @@ public class LDAPStorageProvider implements UserStorageProvider,
s += "*"; s += "*";
} }
conditions.add(conditionsBuilder.equal(UserModel.USERNAME, s.trim().toLowerCase(), 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.NON_ASCII_CHARS_ONLY)); conditions.add(conditionsBuilder.equal(UserModel.EMAIL, s.trim().toLowerCase(), EscapeStrategy.DEFAULT_EXCEPT_ASTERISK));
conditions.add(conditionsBuilder.equal(UserModel.FIRST_NAME, s, EscapeStrategy.NON_ASCII_CHARS_ONLY)); conditions.add(conditionsBuilder.equal(UserModel.FIRST_NAME, s, EscapeStrategy.DEFAULT_EXCEPT_ASTERISK));
conditions.add(conditionsBuilder.equal(UserModel.LAST_NAME, s, EscapeStrategy.NON_ASCII_CHARS_ONLY)); conditions.add(conditionsBuilder.equal(UserModel.LAST_NAME, s, EscapeStrategy.DEFAULT_EXCEPT_ASTERISK));
ldapQuery.addWhereCondition(conditionsBuilder.orCondition(conditions.toArray(Condition[]::new))); ldapQuery.addWhereCondition(conditionsBuilder.orCondition(conditions.toArray(Condition[]::new)));
} }

View file

@ -24,17 +24,35 @@ import java.nio.charset.StandardCharsets;
*/ */
public enum EscapeStrategy { public enum EscapeStrategy {
/**
// LDAP special characters like * ( ) \ are not escaped. Only non-ASCII characters like é are escaped * LDAP special character * is not escaped, other special characters are escaped. Non-ASCII characters like é are escaped.
NON_ASCII_CHARS_ONLY { * Use it for searches where wildcards are allowed.
*/
DEFAULT_EXCEPT_ASTERISK {
@Override @Override
public String escape(String input) { public String escape(String input) {
StringBuilder output = new StringBuilder(); StringBuilder output = new StringBuilder();
for (byte b : input.getBytes(StandardCharsets.UTF_8)) { 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); appendByte(b, output);
} }
}
}
return output.toString(); 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 { DEFAULT {

View file

@ -27,16 +27,16 @@ import org.keycloak.storage.ldap.idm.query.EscapeStrategy;
public class EscapeTest { public class EscapeTest {
@Test @Test
public void testNoAsciiOnlyEscaping() throws Exception { public void testEscapingExceptAsterisk() {
String text = "Véronique* Martin(john)second\\fff//eee\u0000"; 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 #çà"; 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 @Test
public void testEscaping() throws Exception { public void testEscaping() {
String text = "Véronique* Martin(john)second\\fff//eee\u0000"; 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"); 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. // 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()); Assert.assertEquals(1, session.users().searchForUserStream(appRealm, "*11*").count());
LDAPTestAsserts.assertUserImported(UserStoragePrivateUtil.userLocalStorage(session), appRealm, "username11", "John11", "Doel11", "user11@email.org", "124"); 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());
}); });
} }