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\y
as 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\x
as 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