-
Bug
-
Resolution: Fixed
-
P4
-
5.0u20
-
None
-
b78
-
unknown
-
other
FULL PRODUCT VERSION :
java version "1.5.0_20"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.5.0_20-b02-315)
Java HotSpot(TM) Client VM (build 1.5.0_20-141, mixed mode, sharing)
A DESCRIPTION OF THE PROBLEM :
JNDI LDAP
InitialLdapContext does not copy (or clone) non-String environment property values. In particular, if the value of SECURITY_CREDENTIALS is a byte[], the value stored in the Context can be corrupted through the original reference. If the value is corrupted, handling a subsequent LDAP referral will fail because the stored credentials will be invalid for authentication to the referred-to LDAP server.
The implementation of javax.naming.ldap.InitialLdapContext#InitialLdapContext perhaps meets the javadoc contract a bit too literally:
* <p> This constructor will not modify its parameters or
* save references to them, but may save a clone or copy.
The environment argument is saved by a call to java.util.Hashtable#clone:
* Creates a shallow copy of this hashtable. All the structure of the
* hashtable itself is copied, but the keys and values are not cloned.
* This is a relatively expensive operation.
which is inadequate in the case of a mutable Hashtable value (such as a byte[] value for key javax.naming.Context#SECURITY_CREDENTIALS), since the saved value can be subsequently corrupted through the original reference.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Configure a Sun Directory 5.2 deployment with a read-only replica (consumer) and a writable replica (master). Establish an InitialLdapContext to the read-only replica, passing the authentication credentials in a byte[]. Modify the credentials through the original reference. Invoke a modify operation on the Ldap Context. When the read-only replica returns a referral, JNDI will follow the referral and attempt to BIND to the writable replica, which will fail due to the corrupted credentials.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Referral successfully followed; authenticated connection established to the referenced DSA.
ACTUAL -
Following the referral results in an AuthenticationException (Error 49 - Invalid Credentials).
ERROR MESSAGES/STACK TRACES THAT OCCUR :
[LDAP: error code 49 - Invalid Credentials]
REPRODUCIBILITY :
This bug can be reproduced always.
---------- BEGIN SOURCE ----------
/* This test demonstrates a bug in JNDI LDAP
* javax.naming.ldap.InitialLdapContext handling of array type (e.g.,
* byte[]) SECURITY_CREDENTIALS. The passed in array is not copied, so
* a subsequent modification via the external reference can cause
* referrals to fail.
*
* In the affected application, the LDAP account authentication
* credentials are stored in an encrypted data object (in memory and
* serialized to disk). When the clear-text credentials are required,
* the value is decrypted to a UTF-8 byte array. After the call to
* javax.naming.ldap.InitialLdapContext, the clear-text value in the
* byte array is obfuscated by overwriting it with "*" characters in
* order to prevent "leaking" the clear-text value via VM swap.
*
* A typical Sun Directory Server 5.2 deployment includes read-only
* replicas (because there is a limit of four writable replicas),
* which are the instances an application is configured to
* access. Authentication and search operations are handled directly
* by the read-only replica, while any add or update results in a
* referral to a writable replica. Following the referral requires
* JNDI provider to establish an authenticated connection to the
* writable replica. If the credentials are corrupted as described in
* the previous paragraph, this authentication attempt will fail.
*
* The following program uses the reconnect method to simulate the
* referral.
*
* A description of the SECURITY_CREDENTIALS property for LDAP simple
* authentication can be found in
* http://java.sun.com/products/jndi/tutorial/ldap/security/simple.html
*/
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Hashtable;
import javax.naming.directory.*;
import javax.naming.ldap.*;
import javax.naming.*;
public class LdapReconnect {
final static String host = "localhost";
final static String port = "58389";
final static String bindDn = "cn=directory manager";
final static String bindCreds = "etegrity";
public static void main(String[] args) {
Hashtable<String,Object> env = new Hashtable<String,Object>();
env.put(DirContext.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory");
env.put(DirContext.PROVIDER_URL,"ldap://" + host + ":" + port);
env.put(Context.REFERRAL, "follow"); // automatically follow
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, bindDn);
byte[] decryptedPassword = bindCreds.getBytes();
env.put(Context.SECURITY_CREDENTIALS, decryptedPassword);
LdapContext context = null;
try {
System.out.println("Establinshing initial context...");
context = new InitialLdapContext(env, null);
List<String> controls = getSupportedControls(context);
System.out.println("A supported control OID: " + controls.get(0));
// Reconnect: retry LDAP BIND to simulate referral
System.out.println("Reconnecting...");
Control[] connCtls = context.getConnectControls();
context.reconnect(connCtls);
controls = getSupportedControls(context);
System.out.println("A supported control OID: " + controls.get(1));
// Corrupt password from outside of LdapContext, reconnect
try {
System.out.println("Corrupting security credentials...");
Arrays.fill(decryptedPassword, (byte)'*');
connCtls = context.getConnectControls();
context.reconnect(connCtls);
controls = getSupportedControls(context);
System.out.println("A supported control OID: " + controls.get(2));
}
catch (AuthenticationException aex) {
System.out.println("Failed to reconnect: " + aex.getMessage());
}
}
catch (NamingException nex) {
System.err.println("Error: " + nex.getMessage());
}
finally {
if (null != context) {
try {
context.close();
}
catch (NamingException nex2) {
System.err.println("Error closing connection: " + nex2.getMessage());
}
}
}
}
/* Retrieve the "supportedControl" attribute from the DSA */
static List<String> getSupportedControls(LdapContext ctx) throws NamingException {
List<String> results = new ArrayList<String>();
javax.naming.directory.Attribute attr = null;
Exception errorEx = null;
try {
Attributes attrs = ctx.getAttributes("" /* rootDSE */,
new String[]{"supportedcontrol"});
attr = attrs.get("supportedcontrol");
}
catch (NamingException nex) {
throw nex;
}
if (null == attr) {
IllegalArgumentException iax = new IllegalArgumentException(
"Error retrieving supportedConrols from rootDSE");
throw iax;
}
NamingEnumeration nenum = attr.getAll();
while (nenum.hasMore()) {
Object o = nenum.next();
if (o instanceof String) {
results.add((String)o);
}
}
nenum.close();
return results;
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
Configure application to point to writable replica only (not always acceptable to customer).
java version "1.5.0_20"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.5.0_20-b02-315)
Java HotSpot(TM) Client VM (build 1.5.0_20-141, mixed mode, sharing)
A DESCRIPTION OF THE PROBLEM :
JNDI LDAP
InitialLdapContext does not copy (or clone) non-String environment property values. In particular, if the value of SECURITY_CREDENTIALS is a byte[], the value stored in the Context can be corrupted through the original reference. If the value is corrupted, handling a subsequent LDAP referral will fail because the stored credentials will be invalid for authentication to the referred-to LDAP server.
The implementation of javax.naming.ldap.InitialLdapContext#InitialLdapContext perhaps meets the javadoc contract a bit too literally:
* <p> This constructor will not modify its parameters or
* save references to them, but may save a clone or copy.
The environment argument is saved by a call to java.util.Hashtable#clone:
* Creates a shallow copy of this hashtable. All the structure of the
* hashtable itself is copied, but the keys and values are not cloned.
* This is a relatively expensive operation.
which is inadequate in the case of a mutable Hashtable value (such as a byte[] value for key javax.naming.Context#SECURITY_CREDENTIALS), since the saved value can be subsequently corrupted through the original reference.
STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Configure a Sun Directory 5.2 deployment with a read-only replica (consumer) and a writable replica (master). Establish an InitialLdapContext to the read-only replica, passing the authentication credentials in a byte[]. Modify the credentials through the original reference. Invoke a modify operation on the Ldap Context. When the read-only replica returns a referral, JNDI will follow the referral and attempt to BIND to the writable replica, which will fail due to the corrupted credentials.
EXPECTED VERSUS ACTUAL BEHAVIOR :
EXPECTED -
Referral successfully followed; authenticated connection established to the referenced DSA.
ACTUAL -
Following the referral results in an AuthenticationException (Error 49 - Invalid Credentials).
ERROR MESSAGES/STACK TRACES THAT OCCUR :
[LDAP: error code 49 - Invalid Credentials]
REPRODUCIBILITY :
This bug can be reproduced always.
---------- BEGIN SOURCE ----------
/* This test demonstrates a bug in JNDI LDAP
* javax.naming.ldap.InitialLdapContext handling of array type (e.g.,
* byte[]) SECURITY_CREDENTIALS. The passed in array is not copied, so
* a subsequent modification via the external reference can cause
* referrals to fail.
*
* In the affected application, the LDAP account authentication
* credentials are stored in an encrypted data object (in memory and
* serialized to disk). When the clear-text credentials are required,
* the value is decrypted to a UTF-8 byte array. After the call to
* javax.naming.ldap.InitialLdapContext, the clear-text value in the
* byte array is obfuscated by overwriting it with "*" characters in
* order to prevent "leaking" the clear-text value via VM swap.
*
* A typical Sun Directory Server 5.2 deployment includes read-only
* replicas (because there is a limit of four writable replicas),
* which are the instances an application is configured to
* access. Authentication and search operations are handled directly
* by the read-only replica, while any add or update results in a
* referral to a writable replica. Following the referral requires
* JNDI provider to establish an authenticated connection to the
* writable replica. If the credentials are corrupted as described in
* the previous paragraph, this authentication attempt will fail.
*
* The following program uses the reconnect method to simulate the
* referral.
*
* A description of the SECURITY_CREDENTIALS property for LDAP simple
* authentication can be found in
* http://java.sun.com/products/jndi/tutorial/ldap/security/simple.html
*/
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Hashtable;
import javax.naming.directory.*;
import javax.naming.ldap.*;
import javax.naming.*;
public class LdapReconnect {
final static String host = "localhost";
final static String port = "58389";
final static String bindDn = "cn=directory manager";
final static String bindCreds = "etegrity";
public static void main(String[] args) {
Hashtable<String,Object> env = new Hashtable<String,Object>();
env.put(DirContext.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory");
env.put(DirContext.PROVIDER_URL,"ldap://" + host + ":" + port);
env.put(Context.REFERRAL, "follow"); // automatically follow
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, bindDn);
byte[] decryptedPassword = bindCreds.getBytes();
env.put(Context.SECURITY_CREDENTIALS, decryptedPassword);
LdapContext context = null;
try {
System.out.println("Establinshing initial context...");
context = new InitialLdapContext(env, null);
List<String> controls = getSupportedControls(context);
System.out.println("A supported control OID: " + controls.get(0));
// Reconnect: retry LDAP BIND to simulate referral
System.out.println("Reconnecting...");
Control[] connCtls = context.getConnectControls();
context.reconnect(connCtls);
controls = getSupportedControls(context);
System.out.println("A supported control OID: " + controls.get(1));
// Corrupt password from outside of LdapContext, reconnect
try {
System.out.println("Corrupting security credentials...");
Arrays.fill(decryptedPassword, (byte)'*');
connCtls = context.getConnectControls();
context.reconnect(connCtls);
controls = getSupportedControls(context);
System.out.println("A supported control OID: " + controls.get(2));
}
catch (AuthenticationException aex) {
System.out.println("Failed to reconnect: " + aex.getMessage());
}
}
catch (NamingException nex) {
System.err.println("Error: " + nex.getMessage());
}
finally {
if (null != context) {
try {
context.close();
}
catch (NamingException nex2) {
System.err.println("Error closing connection: " + nex2.getMessage());
}
}
}
}
/* Retrieve the "supportedControl" attribute from the DSA */
static List<String> getSupportedControls(LdapContext ctx) throws NamingException {
List<String> results = new ArrayList<String>();
javax.naming.directory.Attribute attr = null;
Exception errorEx = null;
try {
Attributes attrs = ctx.getAttributes("" /* rootDSE */,
new String[]{"supportedcontrol"});
attr = attrs.get("supportedcontrol");
}
catch (NamingException nex) {
throw nex;
}
if (null == attr) {
IllegalArgumentException iax = new IllegalArgumentException(
"Error retrieving supportedConrols from rootDSE");
throw iax;
}
NamingEnumeration nenum = attr.getAll();
while (nenum.hasMore()) {
Object o = nenum.next();
if (o instanceof String) {
results.add((String)o);
}
}
nenum.close();
return results;
}
}
---------- END SOURCE ----------
CUSTOMER SUBMITTED WORKAROUND :
Configure application to point to writable replica only (not always acceptable to customer).