spring-projects / spring-projects/spring-security
LDAP search base require JNDI escaping in addition to LDAP escaping for special characters
@fhanik is already working on this.
Since Dec 13, 2019.
- Dominant language
- Java
- Stars
- 9.6k
- Forks
- 6.3k
- Avg merge
- 2d 11h
- Merged PRs (30d)
- 52
Description
Summary
When use <sec:ldap-authentication-provider> with <sec:password-compare>, user's dn lacks JNDI escaping.
Strictly speaking, method that requires JNDI escaping is used when PasswordComparisonAuthenticator is used.
When passing dn to the Context, javax.naming.Name should be used instead of String.
see: https://docs.oracle.com/javase/jndi/tutorial/beyond/names/syntax.html
To avoid any surprises if a name contains special characters that might conflict with the JNDI composite name syntax, you should use the Context methods that accept a Name.
Actual Behavior
Case 1: User can't sign in with the correct username and password.
- Use
user-dn-pattern="uid={0},ou=people"at<sec:ldap-authentication-provider>. - Input
admin\as existing username and correct password to login form, and Sign in. - (Result) The following message is shown.
Bad credentials
- If do any of the following, sign in succeeds:
- Input
admin\\as username instead ofadmin\. (But granting no authority.) - Use
user-search-filter="(uid={0})"anduser-search-base="ou=people"instead ofuser-dn-pattern="uid={0},ou=people". (But, this has another problemCase 2.) - Use
<sec:ldap-authentication-provider>without<sec:password-compare>. (Also change the password on the LDAP server.) - Use
<sec:authentication-provider>and<sec:ldap-user-service>(using user-search-filter and user-search-base) instead of<sec:ldap-authentication-provider>. (There is no problem.)
- Input
Case 1': Invalid dn is created.
- Use
user-dn-pattern="uid={0},ou=people"at<sec:ldap-authentication-provider>. (same as Case 1) - Input
admin\yas username and some string as password (username and password do not have to be correct) to login form, and Sign in. - (Result) The following message is shown.
*uid=admin\y,ou=people: [LDAP: error code 34 - invalid DN]; nested exception is javax.naming.InvalidNameException: uid=admin\y,ou=people: [LDAP: error code 34 - invalid DN]; remaining name 'uid=admin\y,ou=people'
Case 2: Invalid dn is created when existing username and wrong password are input only.
- Use
user-search-filter="(uid={0})"anduser-search-base="ou=people"at<sec:ldap-authentication-provider>. - Input
admin\xas existing username and wrong password to login form, and Sign in. - (Result) The following message is shown.
uid=admin\x,ou=people: [LDAP: error code 34 - invalid DN]; nested exception is javax.naming.InvalidNameException: uid=admin\x,ou=people: [LDAP: error code 34 - invalid DN]; remaining name 'uid=admin\x,ou=people'
Expected Behavior
Result of Case 1:
- Sign in succeeds as
admin\.
Result of Case 1':
- The following message is shown.
Bad credentials
- If username and password are correct, Sign in succeeds as
admin\y.
Result of Case 2:
- The following message is shown.
Bad credentials
Configuration
Case 1 and Case 1':
<sec:authentication-manager>
<sec:ldap-authentication-provider
user-dn-pattern="uid={0},ou=people"
group-search-base="ou=groups">
<sec:password-compare>
<sec:password-encoder ref="passwordEncoder"/>
</sec:password-compare>
</sec:ldap-authentication-provider>
</sec:authentication-manager>
Case 2:
<sec:authentication-manager>
<sec:ldap-authentication-provider
user-search-base="ou=people"
user-search-filter="(uid={0})"
group-search-base="ou=groups" >
<sec:password-compare>
<sec:password-encoder ref="passwordEncoder"/>
</sec:password-compare>
</sec:ldap-authentication-provider>
</sec:authentication-manager>
Common configuration:
<sec:ldap-server
url="ldap://xxx:389/dc=xxx,dc=org"
manager-dn="cn=xxx,dc=xxx,dc=org"
manager-password="xxx"
/>
Existing User's dn(after LDAP escaping):
uid=admin\\,ou=people,dc=xxx,dc=org(username isadmin\)uid=admin\\x,ou=people,dc=xxx,dc=org(username isadmin\x)
Version
- 5.1.3.RELEASE (I use this version.)
- 5.2.1.RELEASE (latest) (It's reproduced also this version.)
Environment
- Redhat OpenJDK 1.8.0.212-3.b04
- Open LDAP
Sample
Use default Login Form of Spring Security(<sec:form-login/>) and the above configuration.
Analysis
Case 1 (and Case 1'):
At org.springframework.security.ldap.authentication.PasswordComparisonAuthenticator#authenticate(Authentication),
user = ldapTemplate.retrieveEntry(userDn, getUserAttributes());
This userDn is String that value is uid=admin\\,ou=people. (after LDAP escaping)
(When Case 1', the value is uid=admin\\y,ou=people.)
This ldapTemplate is org.springframework.security.ldap.SpringSecurityLdapTemplate.
At org.springframework.security.ldap.SpringSecurityLdapTemplate#retrieveEntry(String, String[]),
Attributes attrs = ctx.getAttributes(dn, attributesToRetrieve);
This dn is String that value is uid=admin\\,ou=people. (after LDAP escaping)
(When Case 1', the value is uid=admin\\y,ou=people.)
This ctx is javax.naming.ldap.InitialLdapContext.
At javax.naming.ldap.InitialLdapContext#getAttributes(String, String[])
return getURLOrDefaultInitDirCtx(name).getAttributes(name, attrIds);
This getURLOrDefaultInitDirCtx(name) returns com.sun.jndi.ldap.LdapCtx object.
(LdapCtx is created by com.sun.jndi.ldap.LdapCtxFactory.)
As a result, at com.sun.jndi.toolkit.ctx.PartialCompositeDirContext#getAttributes(String, String[])
return getAttributes(new CompositeName(name), attrIds);
This name is String that value is uid=admin\\,ou=people. (after LDAP escaping)
(When Case 1', the value is uid=admin\\y,ou=people.)
The constructor of CompositeName JNDI unescape the argument.
(Therefore, in Case 1, \\, of uid=admin\\,ou=people becomes \, meaning LDAP escaped ,. And in Case 1', \\y, of uid=admin\\y,ou=people becomes an invalid sequence \y,.)
This is an extra unescaping.
(If new CompositeName().add(name) is used instead of new CompositeName(name),
this extra unescaping is not performed.)
If LdapCtxFactory creates com.sun.jndi.ldap.LdapReferralContext instead of LdapCtx,
LdapReferralContext#getAttributes(String, String[]) is executed,
and it execute new CompositeName().add(name) instead of new CompositeName(name).
(However, I have never seen LdapReferralContext created.)
SpringSecurityLdapTemplate should also have a method that accepts dn as javax.naming.Name instead of String.
And new method SpringSecurityLdapTemplate#retrieveEntry(Name, String[]) should use javax.naming.ldap.InitialLdapContext#getAttributes(Name, String[]).
PasswordComparisonAuthenticator#authenticate(Authentication) should use
new method SpringSecurityLdapTemplate#retrieveEntry(Name, String[]) instead of SpringSecurityLdapTemplate#retrieveEntry(String, String[]),
and Name should be created as one of the following:
- new CompositeName().add(name)
- new LdapName(name) (
new CompositeName().add(name)is called by class of JDK.)
see: https://docs.oracle.com/javase/jndi/tutorial/beyond/names/syntax.html
Case 2:
When existing username and wrong password are input,
org.springframework.security.ldap.authentication.PasswordComparisonAuthenticator#isLdapPasswordCompare(DirContextOperations, SpringSecurityLdapTemplate, String)
is executed.
(This method is not executed, when correct password.)
At org.springframework.security.ldap.authentication.PasswordComparisonAuthenticator#isLdapPasswordCompare(DirContextOperations, SpringSecurityLdapTemplate, String),
return ldapTemplate.compare(user.getDn().toString(), passwordAttributeName,
passwordBytes);
This user.getDn().toString() returns uid=admin\\x,ou=people. (after LDAP escaping)
This ldapTemplate is org.springframework.security.ldap.SpringSecurityLdapTemplate.
At org.springframework.security.ldap.SpringSecurityLdapTemplate#compare(String, String, Object)
NamingEnumeration results = ctx.search(dn,
comparisonFilter, new Object[] { value }, ctls);
This dn is String that value is uid=admin\\x,ou=people. (after LDAP escaping)
As a result, at com.sun.jndi.toolkit.ctx.PartialCompositeDirContext#search(String, String, Object[], SearchControls)
return search(new CompositeName(name), filterExpr, filterArgs, cons);
This name is String that value is uid=admin\\x,ou=people. (after LDAP escaping)
The constructor of CompositeName JNDI unescape the argument.
This is an extra unescaping.
Again, like Case 1, PasswordComparisonAuthenticator#isLdapPasswordCompare and SpringSecurityLdapTemplate#compare should pass dn as javax.naming.Name instead of String.
see: https://docs.oracle.com/javase/jndi/tutorial/beyond/names/syntax.html
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.