Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-6886058

JNDI LDAP InitialLdapContext SECURITY_CREDENTIALS byte[] can be corrupted, then LDAP referral fails

XMLWordPrintable

    • Icon: Bug Bug
    • Resolution: Fixed
    • Icon: P4 P4
    • 7
    • 5.0u20
    • core-libs
    • 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).

            weijun Weijun Wang
            sspage Scott Page (Inactive)
            Votes:
            0 Vote for this issue
            Watchers:
            0 Start watching this issue

              Created:
              Updated:
              Resolved:
              Imported:
              Indexed: