diff --git a/tomcat-9.0-CVE-2021-30640.patch b/tomcat-9.0-CVE-2021-30640.patch new file mode 100644 index 0000000..fd19e88 --- /dev/null +++ b/tomcat-9.0-CVE-2021-30640.patch @@ -0,0 +1,2666 @@ +Index: apache-tomcat-9.0.43-src/build.properties.default +=================================================================== +--- apache-tomcat-9.0.43-src.orig/build.properties.default ++++ apache-tomcat-9.0.43-src/build.properties.default +@@ -244,6 +244,15 @@ objenesis.home=${base.path}/objenesis-${ + objenesis.jar=${objenesis.home}/objenesis-${objenesis.version}.jar + objenesis.loc=${base-maven.loc}/org/objenesis/objenesis/${objenesis.version}/objenesis-${objenesis.version}.jar + ++# ----- UnboundID, used by unit tests, version 5.1.4 or later ----- ++unboundid.version=5.1.4 ++unboundid.checksum.enabled=true ++unboundid.checksum.algorithm=SHA-512 ++unboundid.checksum.value=04cf7f59eddebdd5b51e5be55021f9d9c667cca6101eac954e7a8d5b51f4c23372cd8f041640157f082435a166b75d85e79252b516130ede7d966dae6d3eae67 ++unboundid.home=${base.path}/unboundid-${unboundid.version} ++unboundid.jar=${unboundid.home}/unboundid-ldapsdk-${unboundid.version}.jar ++unboundid.loc=${base-maven.loc}/com/unboundid/unboundid-ldapsdk/${unboundid.version}/unboundid-ldapsdk-${unboundid.version}.jar ++ + # ----- Checkstyle, version 6.16 or later ----- + checkstyle.version=8.22 + checkstyle.checksum.enabled=true +Index: apache-tomcat-9.0.43-src/build.xml +=================================================================== +--- apache-tomcat-9.0.43-src.orig/build.xml ++++ apache-tomcat-9.0.43-src/build.xml +@@ -3164,6 +3164,15 @@ skip.installer property in build.propert + + + ++ ++ ++ ++ ++ ++ ++ ++ ++ + + + connectionPool = null; + +- + /** + * The pool size limit. If 1, pooling is not used. + */ + protected int connectionPoolSize = 1; + +- + /** + * Whether to use context ClassLoader or default ClassLoader. + * True means use context ClassLoader, and True is the default +@@ -504,37 +476,35 @@ public class JNDIRealm extends RealmBase + return forceDnHexEscape; + } + ++ + public void setForceDnHexEscape(boolean forceDnHexEscape) { + this.forceDnHexEscape = forceDnHexEscape; + } + ++ + /** + * @return the type of authentication to use. + */ + public String getAuthentication() { +- + return authentication; +- + } + ++ + /** + * Set the type of authentication to use. + * + * @param authentication The authentication + */ + public void setAuthentication(String authentication) { +- + this.authentication = authentication; +- + } + ++ + /** + * @return the connection username for this Realm. + */ + public String getConnectionName() { +- + return this.connectionName; +- + } + + +@@ -544,9 +514,7 @@ public class JNDIRealm extends RealmBase + * @param connectionName The new connection username + */ + public void setConnectionName(String connectionName) { +- + this.connectionName = connectionName; +- + } + + +@@ -554,9 +522,7 @@ public class JNDIRealm extends RealmBase + * @return the connection password for this Realm. + */ + public String getConnectionPassword() { +- + return this.connectionPassword; +- + } + + +@@ -566,9 +532,7 @@ public class JNDIRealm extends RealmBase + * @param connectionPassword The new connection password + */ + public void setConnectionPassword(String connectionPassword) { +- + this.connectionPassword = connectionPassword; +- + } + + +@@ -576,9 +540,7 @@ public class JNDIRealm extends RealmBase + * @return the connection URL for this Realm. + */ + public String getConnectionURL() { +- + return this.connectionURL; +- + } + + +@@ -588,9 +550,7 @@ public class JNDIRealm extends RealmBase + * @param connectionURL The new connection URL + */ + public void setConnectionURL(String connectionURL) { +- + this.connectionURL = connectionURL; +- + } + + +@@ -598,9 +558,7 @@ public class JNDIRealm extends RealmBase + * @return the JNDI context factory for this Realm. + */ + public String getContextFactory() { +- + return this.contextFactory; +- + } + + +@@ -610,11 +568,10 @@ public class JNDIRealm extends RealmBase + * @param contextFactory The new context factory + */ + public void setContextFactory(String contextFactory) { +- + this.contextFactory = contextFactory; +- + } + ++ + /** + * @return the derefAliases setting to be used. + */ +@@ -622,33 +579,32 @@ public class JNDIRealm extends RealmBase + return derefAliases; + } + ++ + /** + * Set the value for derefAliases to be used when searching the directory. + * + * @param derefAliases New value of property derefAliases. + */ + public void setDerefAliases(java.lang.String derefAliases) { +- this.derefAliases = derefAliases; ++ this.derefAliases = derefAliases; + } + ++ + /** + * @return the protocol to be used. + */ + public String getProtocol() { +- + return protocol; +- + } + ++ + /** + * Set the protocol for this Realm. + * + * @param protocol The new protocol. + */ + public void setProtocol(String protocol) { +- + this.protocol = protocol; +- + } + + +@@ -692,9 +648,7 @@ public class JNDIRealm extends RealmBase + * @return the base element for user searches. + */ + public String getUserBase() { +- + return this.userBase; +- + } + + +@@ -704,9 +658,7 @@ public class JNDIRealm extends RealmBase + * @param userBase The new base element + */ + public void setUserBase(String userBase) { +- + this.userBase = userBase; +- + } + + +@@ -714,9 +666,7 @@ public class JNDIRealm extends RealmBase + * @return the message format pattern for selecting users in this Realm. + */ + public String getUserSearch() { +- + return this.userSearch; +- + } + + +@@ -745,9 +695,7 @@ public class JNDIRealm extends RealmBase + * @return the "search subtree for users" flag. + */ + public boolean getUserSubtree() { +- + return this.userSubtree; +- + } + + +@@ -757,9 +705,7 @@ public class JNDIRealm extends RealmBase + * @param userSubtree The new search flag + */ + public void setUserSubtree(boolean userSubtree) { +- + this.userSubtree = userSubtree; +- + } + + +@@ -767,7 +713,6 @@ public class JNDIRealm extends RealmBase + * @return the user role name attribute name for this Realm. + */ + public String getUserRoleName() { +- + return userRoleName; + } + +@@ -778,9 +723,7 @@ public class JNDIRealm extends RealmBase + * @param userRoleName The new userRole name attribute name + */ + public void setUserRoleName(String userRoleName) { +- + this.userRoleName = userRoleName; +- + } + + +@@ -788,9 +731,7 @@ public class JNDIRealm extends RealmBase + * @return the base element for role searches. + */ + public String getRoleBase() { +- + return this.roleBase; +- + } + + +@@ -809,9 +750,7 @@ public class JNDIRealm extends RealmBase + * @return the role name attribute name for this Realm. + */ + public String getRoleName() { +- + return this.roleName; +- + } + + +@@ -821,9 +760,7 @@ public class JNDIRealm extends RealmBase + * @param roleName The new role name attribute name + */ + public void setRoleName(String roleName) { +- + this.roleName = roleName; +- + } + + +@@ -831,9 +768,7 @@ public class JNDIRealm extends RealmBase + * @return the message format pattern for selecting roles in this Realm. + */ + public String getRoleSearch() { +- + return this.roleSearch; +- + } + + +@@ -862,9 +797,7 @@ public class JNDIRealm extends RealmBase + * @return the "search subtree for roles" flag. + */ + public boolean getRoleSubtree() { +- + return this.roleSubtree; +- + } + + +@@ -874,18 +807,15 @@ public class JNDIRealm extends RealmBase + * @param roleSubtree The new search flag + */ + public void setRoleSubtree(boolean roleSubtree) { +- + this.roleSubtree = roleSubtree; +- + } + ++ + /** + * @return the "The nested group search flag" flag. + */ + public boolean getRoleNested() { +- + return this.roleNested; +- + } + + +@@ -895,9 +825,7 @@ public class JNDIRealm extends RealmBase + * @param roleNested The nested group search flag + */ + public void setRoleNested(boolean roleNested) { +- + this.roleNested = roleNested; +- + } + + +@@ -905,9 +833,7 @@ public class JNDIRealm extends RealmBase + * @return the password attribute used to retrieve the user password. + */ + public String getUserPassword() { +- + return this.userPassword; +- + } + + +@@ -917,9 +843,7 @@ public class JNDIRealm extends RealmBase + * @param userPassword The new password attribute + */ + public void setUserPassword(String userPassword) { +- + this.userPassword = userPassword; +- + } + + +@@ -927,6 +851,7 @@ public class JNDIRealm extends RealmBase + return userRoleAttribute; + } + ++ + public void setUserRoleAttribute(String userRoleAttribute) { + this.userRoleAttribute = userRoleAttribute; + } +@@ -935,14 +860,10 @@ public class JNDIRealm extends RealmBase + * @return the message format pattern for selecting users in this Realm. + */ + public String getUserPattern() { +- + return this.userPattern; +- + } + + +- +- + /** + * Set the message format pattern for selecting users in this Realm. + * This may be one simple pattern, or multiple patterns to be tried, +@@ -970,9 +891,7 @@ public class JNDIRealm extends RealmBase + * @return Value of property alternateURL. + */ + public String getAlternateURL() { +- + return this.alternateURL; +- + } + + +@@ -982,9 +901,7 @@ public class JNDIRealm extends RealmBase + * @param alternateURL New value of property alternateURL. + */ + public void setAlternateURL(String alternateURL) { +- + this.alternateURL = alternateURL; +- + } + + +@@ -992,9 +909,7 @@ public class JNDIRealm extends RealmBase + * @return the common role + */ + public String getCommonRole() { +- + return commonRole; +- + } + + +@@ -1004,9 +919,7 @@ public class JNDIRealm extends RealmBase + * @param commonRole The common role + */ + public void setCommonRole(String commonRole) { +- + this.commonRole = commonRole; +- + } + + +@@ -1014,9 +927,7 @@ public class JNDIRealm extends RealmBase + * @return the connection timeout. + */ + public String getConnectionTimeout() { +- + return connectionTimeout; +- + } + + +@@ -1026,18 +937,15 @@ public class JNDIRealm extends RealmBase + * @param timeout The new connection timeout + */ + public void setConnectionTimeout(String timeout) { +- + this.connectionTimeout = timeout; +- + } + ++ + /** + * @return the read timeout. + */ + public String getReadTimeout() { +- + return readTimeout; +- + } + + +@@ -1047,9 +955,7 @@ public class JNDIRealm extends RealmBase + * @param timeout The new read timeout + */ + public void setReadTimeout(String timeout) { +- + this.readTimeout = timeout; +- + } + + +@@ -1077,6 +983,7 @@ public class JNDIRealm extends RealmBase + return useDelegatedCredential; + } + ++ + public void setUseDelegatedCredential(boolean useDelegatedCredential) { + this.useDelegatedCredential = useDelegatedCredential; + } +@@ -1086,6 +993,7 @@ public class JNDIRealm extends RealmBase + return spnegoDelegationQop; + } + ++ + public void setSpnegoDelegationQop(String spnegoDelegationQop) { + this.spnegoDelegationQop = spnegoDelegationQop; + } +@@ -1098,6 +1006,7 @@ public class JNDIRealm extends RealmBase + return useStartTls; + } + ++ + /** + * Flag whether StartTLS should be used when connecting to the ldap server + * +@@ -1109,6 +1018,7 @@ public class JNDIRealm extends RealmBase + this.useStartTls = useStartTls; + } + ++ + /** + * @return list of the allowed cipher suites when connections are made using + * StartTLS +@@ -1128,6 +1038,7 @@ public class JNDIRealm extends RealmBase + return this.cipherSuitesArray; + } + ++ + /** + * Set the allowed cipher suites when opening a connection using StartTLS. + * The cipher suites are expected as a comma separated list. +@@ -1139,6 +1050,7 @@ public class JNDIRealm extends RealmBase + this.cipherSuites = suites; + } + ++ + /** + * @return the connection pool size, or the default value 1 if pooling + * is disabled +@@ -1147,6 +1059,7 @@ public class JNDIRealm extends RealmBase + return connectionPoolSize; + } + ++ + /** + * Set the connection pool size + * @param connectionPoolSize the new pool size +@@ -1155,6 +1068,7 @@ public class JNDIRealm extends RealmBase + this.connectionPoolSize = connectionPoolSize; + } + ++ + /** + * @return name of the {@link HostnameVerifier} class used for connections + * using StartTLS, or the empty string, if the default verifier +@@ -1167,6 +1081,7 @@ public class JNDIRealm extends RealmBase + return this.hostnameVerifier.getClass().getCanonicalName(); + } + ++ + /** + * Set the {@link HostnameVerifier} to be used when opening connections + * using StartTLS. An instance of the given class name will be constructed +@@ -1183,6 +1098,7 @@ public class JNDIRealm extends RealmBase + } + } + ++ + /** + * @return the {@link HostnameVerifier} to use for peer certificate + * verification when opening connections using StartTLS. +@@ -1191,8 +1107,7 @@ public class JNDIRealm extends RealmBase + if (this.hostnameVerifier != null) { + return this.hostnameVerifier; + } +- if (this.hostNameVerifierClassName == null +- || hostNameVerifierClassName.equals("")) { ++ if (this.hostNameVerifierClassName == null || hostNameVerifierClassName.equals("")) { + return null; + } + try { +@@ -1212,6 +1127,7 @@ public class JNDIRealm extends RealmBase + } + } + ++ + /** + * Set the {@link SSLSocketFactory} to be used when opening connections + * using StartTLS. An instance of the factory with the given name will be +@@ -1225,6 +1141,7 @@ public class JNDIRealm extends RealmBase + this.sslSocketFactoryClassName = factoryClassName; + } + ++ + /** + * Set the ssl protocol to be used for connections using StartTLS. + * +@@ -1235,6 +1152,7 @@ public class JNDIRealm extends RealmBase + this.sslProtocol = protocol; + } + ++ + /** + * @return the list of supported ssl protocols by the default + * {@link SSLContext} +@@ -1248,12 +1166,14 @@ public class JNDIRealm extends RealmBase + } + } + ++ + private Object constructInstance(String className) + throws ReflectiveOperationException { + Class clazz = Class.forName(className); + return clazz.getConstructor().newInstance(); + } + ++ + /** + * Sets whether to use the context or default ClassLoader. + * True means use context ClassLoader. +@@ -1264,6 +1184,7 @@ public class JNDIRealm extends RealmBase + useContextClassLoader = useContext; + } + ++ + /** + * Returns whether to use the context or default ClassLoader. + * True means to use the context ClassLoader. +@@ -1274,6 +1195,7 @@ public class JNDIRealm extends RealmBase + return useContextClassLoader; + } + ++ + // ---------------------------------------------------------- Realm Methods + + /** +@@ -1333,7 +1255,7 @@ public class JNDIRealm extends RealmBase + closePooledConnections(); + + // open a new directory context. +- connection = get(); ++ connection = get(true); + + // Try the authentication again. + principal = authenticate(connection, username, credentials); +@@ -1356,21 +1278,14 @@ public class JNDIRealm extends RealmBase + closePooledConnections(); + + // Return "not authenticated" for this request +- if (containerLog.isDebugEnabled()) ++ if (containerLog.isDebugEnabled()) { + containerLog.debug("Returning null principal."); ++ } + return null; +- + } +- + } + + +- // -------------------------------------------------------- Package Methods +- +- +- // ------------------------------------------------------ Protected Methods +- +- + /** + * Return the Principal associated with the specified username and + * credentials, if there is one; otherwise return null. +@@ -1383,22 +1298,18 @@ public class JNDIRealm extends RealmBase + * + * @exception NamingException if a directory server error occurs + */ +- public Principal authenticate(JNDIConnection connection, +- String username, +- String credentials) +- throws NamingException { +- +- if (username == null || username.equals("") +- || credentials == null || credentials.equals("")) { +- if (containerLog.isDebugEnabled()) ++ public Principal authenticate(JNDIConnection connection, String username, String credentials) ++ throws NamingException { ++ ++ if (username == null || username.equals("") || credentials == null || credentials.equals("")) { ++ if (containerLog.isDebugEnabled()) { + containerLog.debug("username null or empty: returning null principal."); ++ } + return null; + } + + if (userPatternArray != null) { +- for (int curUserPattern = 0; +- curUserPattern < userPatternArray.length; +- curUserPattern++) { ++ for (int curUserPattern = 0; curUserPattern < userPatternArray.length; curUserPattern++) { + // Retrieve user information + User user = getUser(connection, username, credentials, curUserPattern); + if (user != null) { +@@ -1426,12 +1337,14 @@ public class JNDIRealm extends RealmBase + } else { + // Retrieve user information + User user = getUser(connection, username, credentials); +- if (user == null) ++ if (user == null) { + return null; ++ } + + // Check the user's credentials +- if (!checkCredentials(connection.context, user, credentials)) ++ if (!checkCredentials(connection.context, user, credentials)) { + return null; ++ } + + // Search for additional roles + List roles = getRoles(connection, user); +@@ -1445,6 +1358,8 @@ public class JNDIRealm extends RealmBase + } + + ++ // ------------------------------------------------------ Protected Methods ++ + /** + * Return a User object containing information about the user + * with the specified username, if found in the directory; +@@ -1457,9 +1372,7 @@ public class JNDIRealm extends RealmBase + * + * @see #getUser(JNDIConnection, String, String, int) + */ +- protected User getUser(JNDIConnection connection, String username) +- throws NamingException { +- ++ protected User getUser(JNDIConnection connection, String username) throws NamingException { + return getUser(connection, username, null, -1); + } + +@@ -1477,9 +1390,7 @@ public class JNDIRealm extends RealmBase + * + * @see #getUser(JNDIConnection, String, String, int) + */ +- protected User getUser(JNDIConnection connection, String username, String credentials) +- throws NamingException { +- ++ protected User getUser(JNDIConnection connection, String username, String credentials) throws NamingException { + return getUser(connection, username, credentials, -1); + } + +@@ -1502,18 +1413,19 @@ public class JNDIRealm extends RealmBase + * @return the User object + * @exception NamingException if a directory server error occurs + */ +- protected User getUser(JNDIConnection connection, String username, +- String credentials, int curUserPattern) +- throws NamingException { ++ protected User getUser(JNDIConnection connection, String username, String credentials, int curUserPattern) ++ throws NamingException { + + User user = null; + + // Get attributes to retrieve from user entry + List list = new ArrayList<>(); +- if (userPassword != null) ++ if (userPassword != null) { + list.add(userPassword); +- if (userRoleName != null) ++ } ++ if (userRoleName != null) { + list.add(userRoleName); ++ } + if (userRoleAttribute != null) { + list.add(userRoleAttribute); + } +@@ -1545,8 +1457,7 @@ public class JNDIRealm extends RealmBase + if (userPassword == null && credentials != null && user != null) { + // The password is available. Insert it since it may be required for + // role searches. +- return new User(user.getUserName(), user.getDN(), credentials, +- user.getRoles(), user.getUserRoleId()); ++ return new User(user.getUserName(), user.getDN(), credentials, user.getRoles(), user.getUserRoleId()); + } + + return user; +@@ -1566,11 +1477,8 @@ public class JNDIRealm extends RealmBase + * @return the User object + * @exception NamingException if a directory server error occurs + */ +- protected User getUserByPattern(DirContext context, +- String username, +- String[] attrIds, +- String dn) +- throws NamingException { ++ protected User getUserByPattern(DirContext context, String username, String[] attrIds, String dn) ++ throws NamingException { + + // If no attributes are requested, no need to look for them + if (attrIds == null || attrIds.length == 0) { +@@ -1584,13 +1492,15 @@ public class JNDIRealm extends RealmBase + } catch (NameNotFoundException e) { + return null; + } +- if (attrs == null) ++ if (attrs == null) { + return null; ++ } + + // Retrieve value of userPassword + String password = null; +- if (userPassword != null) ++ if (userPassword != null) { + password = getAttributeValue(userPassword, attrs); ++ } + + String userRoleAttrValue = null; + if (userRoleAttribute != null) { +@@ -1599,8 +1509,9 @@ public class JNDIRealm extends RealmBase + + // Retrieve values of userRoleName attribute + ArrayList roles = null; +- if (userRoleName != null) ++ if (userRoleName != null) { + roles = addAttributeValues(userRoleName, attrs, roles); ++ } + + return new User(username, dn, password, roles, userRoleAttrValue); + } +@@ -1621,20 +1532,20 @@ public class JNDIRealm extends RealmBase + * @exception NamingException if a directory server error occurs + * @see #getUserByPattern(DirContext, String, String[], String) + */ +- protected User getUserByPattern(JNDIConnection connection, +- String username, +- String credentials, +- String[] attrIds, +- int curUserPattern) +- throws NamingException { ++ protected User getUserByPattern(JNDIConnection connection, String username, String credentials, String[] attrIds, ++ int curUserPattern) throws NamingException { + + User user = null; + +- if (username == null || userPatternArray[curUserPattern] == null) ++ if (username == null || userPatternArray[curUserPattern] == null) { + return null; ++ } + +- // Form the dn from the user pattern +- String dn = connection.userPatternFormatArray[curUserPattern].format(new String[] { username }); ++ // Form the DistinguishedName from the user pattern. ++ // Escape in case username contains a character with special meaning in ++ // an attribute value. ++ String dn = connection.userPatternFormatArray[curUserPattern].format( ++ new String[] { doAttributeValueEscaping(username) }); + + try { + user = getUserByPattern(connection.context, username, attrIds, dn); +@@ -1666,16 +1577,17 @@ public class JNDIRealm extends RealmBase + * @return the User object + * @exception NamingException if a directory server error occurs + */ +- protected User getUserBySearch(JNDIConnection connection, +- String username, +- String[] attrIds) +- throws NamingException { ++ protected User getUserBySearch(JNDIConnection connection, String username, String[] attrIds) ++ throws NamingException { + +- if (username == null || connection.userSearchFormat == null) ++ if (username == null || connection.userSearchFormat == null) { + return null; ++ } + + // Form the search filter +- String filter = connection.userSearchFormat.format(new String[] { username }); ++ // Escape in case username contains a character with special meaning in ++ // a search filter. ++ String filter = connection.userSearchFormat.format(new String[] { doFilterEscaping(username) }); + + // Set up the search controls + SearchControls constraints = new SearchControls(); +@@ -1690,12 +1602,12 @@ public class JNDIRealm extends RealmBase + constraints.setTimeLimit(timeLimit); + + // Specify the attributes to be retrieved +- if (attrIds == null) ++ if (attrIds == null) { + attrIds = new String[0]; ++ } + constraints.setReturningAttributes(attrIds); + +- NamingEnumeration results = +- connection.context.search(userBase, filter, constraints); ++ NamingEnumeration results = connection.context.search(userBase, filter, constraints); + + try { + // Fail if no entries found +@@ -1704,10 +1616,11 @@ public class JNDIRealm extends RealmBase + return null; + } + } catch (PartialResultException ex) { +- if (!adCompat) ++ if (!adCompat) { + throw ex; +- else ++ } else { + return null; ++ } + } + + // Get result for the first entry found +@@ -1722,24 +1635,28 @@ public class JNDIRealm extends RealmBase + return null; + } + } catch (PartialResultException ex) { +- if (!adCompat) ++ if (!adCompat) { + throw ex; ++ } + } + + String dn = getDistinguishedName(connection.context, userBase, result); + +- if (containerLog.isTraceEnabled()) ++ if (containerLog.isTraceEnabled()) { + containerLog.trace(" entry found for " + username + " with dn " + dn); ++ } + + // Get the entry's attributes + Attributes attrs = result.getAttributes(); +- if (attrs == null) ++ if (attrs == null) { + return null; ++ } + + // Retrieve value of userPassword + String password = null; +- if (userPassword != null) ++ if (userPassword != null) { + password = getAttributeValue(userPassword, attrs); ++ } + + String userRoleAttrValue = null; + if (userRoleAttribute != null) { +@@ -1748,8 +1665,9 @@ public class JNDIRealm extends RealmBase + + // Retrieve values of userRoleName attribute + ArrayList roles = null; +- if (userRoleName != null) ++ if (userRoleName != null) { + roles = addAttributeValues(userRoleName, attrs, roles); ++ } + + return new User(username, dn, password, roles, userRoleAttrValue); + } finally { +@@ -1775,30 +1693,25 @@ public class JNDIRealm extends RealmBase + * @return true if the credentials are validated + * @exception NamingException if a directory server error occurs + */ +- protected boolean checkCredentials(DirContext context, +- User user, +- String credentials) +- throws NamingException { +- +- boolean validated = false; +- +- if (userPassword == null) { +- validated = bindAsUser(context, user, credentials); +- } else { +- validated = compareCredentials(context, user, credentials); +- } +- +- if (containerLog.isTraceEnabled()) { +- if (validated) { +- containerLog.trace(sm.getString("jndiRealm.authenticateSuccess", +- user.getUserName())); +- } else { +- containerLog.trace(sm.getString("jndiRealm.authenticateFailure", +- user.getUserName())); +- } +- } +- return validated; +- } ++ protected boolean checkCredentials(DirContext context, User user, String credentials) throws NamingException { ++ ++ boolean validated = false; ++ ++ if (userPassword == null) { ++ validated = bindAsUser(context, user, credentials); ++ } else { ++ validated = compareCredentials(context, user, credentials); ++ } ++ ++ if (containerLog.isTraceEnabled()) { ++ if (validated) { ++ containerLog.trace(sm.getString("jndiRealm.authenticateSuccess", user.getUserName())); ++ } else { ++ containerLog.trace(sm.getString("jndiRealm.authenticateFailure", user.getUserName())); ++ } ++ } ++ return validated; ++ } + + + /** +@@ -1811,17 +1724,15 @@ public class JNDIRealm extends RealmBase + * @return true if the credentials are validated + * @exception NamingException if a directory server error occurs + */ +- protected boolean compareCredentials(DirContext context, +- User info, +- String credentials) +- throws NamingException { +- ++ protected boolean compareCredentials(DirContext context, User info, String credentials) throws NamingException { + // Validate the credentials specified by the user +- if (containerLog.isTraceEnabled()) ++ if (containerLog.isTraceEnabled()) { + containerLog.trace(" validating credentials"); ++ } + +- if (info == null || credentials == null) ++ if (info == null || credentials == null) { + return false; ++ } + + String password = info.getPassword(); + +@@ -1838,21 +1749,22 @@ public class JNDIRealm extends RealmBase + * @return true if the credentials are validated + * @exception NamingException if a directory server error occurs + */ +- protected boolean bindAsUser(DirContext context, +- User user, +- String credentials) +- throws NamingException { +- +- if (credentials == null || user == null) +- return false; +- +- String dn = user.getDN(); +- if (dn == null) +- return false; +- +- // Validate the credentials specified by the user +- if (containerLog.isTraceEnabled()) { +- containerLog.trace(" validating credentials by binding as the user"); ++ protected boolean bindAsUser(DirContext context, User user, String credentials) throws NamingException { ++ ++ if (credentials == null || user == null) { ++ return false; ++ } ++ ++ // This is returned from the directory so will be attribute value ++ // escaped if required ++ String dn = user.getDN(); ++ if (dn == null) { ++ return false; ++ } ++ ++ // Validate the credentials specified by the user ++ if (containerLog.isTraceEnabled()) { ++ containerLog.trace(" validating credentials by binding as the user"); + } + + userCredentialsAdd(context, dn, credentials); +@@ -1877,48 +1789,47 @@ public class JNDIRealm extends RealmBase + return validated; + } + +- /** +- * Configure the context to use the provided credentials for +- * authentication. +- * +- * @param context DirContext to configure +- * @param dn Distinguished name of user +- * @param credentials Credentials of user +- * @exception NamingException if a directory server error occurs +- */ +- private void userCredentialsAdd(DirContext context, String dn, +- String credentials) throws NamingException { ++ ++ /** ++ * Configure the context to use the provided credentials for ++ * authentication. ++ * ++ * @param context DirContext to configure ++ * @param dn Distinguished name of user ++ * @param credentials Credentials of user ++ * @exception NamingException if a directory server error occurs ++ */ ++ private void userCredentialsAdd(DirContext context, String dn, String credentials) throws NamingException { + // Set up security environment to bind as the user + context.addToEnvironment(Context.SECURITY_PRINCIPAL, dn); + context.addToEnvironment(Context.SECURITY_CREDENTIALS, credentials); + } + ++ + /** + * Configure the context to use {@link #connectionName} and + * {@link #connectionPassword} if specified or an anonymous connection if + * those attributes are not specified. + * +- * @param context DirContext to configure +- * @exception NamingException if a directory server error occurs ++ * @param context DirContext to configure ++ * @exception NamingException if a directory server error occurs + */ +- private void userCredentialsRemove(DirContext context) +- throws NamingException { ++ private void userCredentialsRemove(DirContext context) throws NamingException { + // Restore the original security environment + if (connectionName != null) { +- context.addToEnvironment(Context.SECURITY_PRINCIPAL, +- connectionName); ++ context.addToEnvironment(Context.SECURITY_PRINCIPAL, connectionName); + } else { + context.removeFromEnvironment(Context.SECURITY_PRINCIPAL); + } + + if (connectionPassword != null) { +- context.addToEnvironment(Context.SECURITY_CREDENTIALS, +- connectionPassword); ++ context.addToEnvironment(Context.SECURITY_CREDENTIALS, connectionPassword); + } else { + context.removeFromEnvironment(Context.SECURITY_CREDENTIALS); + } + } + ++ + /** + * Return a List of roles associated with the given User. Any + * roles present in the user's directory entry are supplemented by +@@ -1930,21 +1841,27 @@ public class JNDIRealm extends RealmBase + * @return the list of role names + * @exception NamingException if a directory server error occurs + */ +- protected List getRoles(JNDIConnection connection, User user) +- throws NamingException { ++ protected List getRoles(JNDIConnection connection, User user) throws NamingException { + +- if (user == null) ++ if (user == null) { + return null; ++ } + ++ // This is returned from the directory so will be attribute value ++ // escaped if required + String dn = user.getDN(); ++ // This is the name the user provided to the authentication process so ++ // it will not be escaped + String username = user.getUserName(); + String userRoleId = user.getUserRoleId(); + +- if (dn == null || username == null) ++ if (dn == null || username == null) { + return null; ++ } + +- if (containerLog.isTraceEnabled()) ++ if (containerLog.isTraceEnabled()) { + containerLog.trace(" getRoles(" + dn + ")"); ++ } + + // Start with roles retrieved from the user entry + List list = new ArrayList<>(); +@@ -1952,8 +1869,9 @@ public class JNDIRealm extends RealmBase + if (userRoles != null) { + list.addAll(userRoles); + } +- if (commonRole != null) ++ if (commonRole != null) { + list.add(commonRole); ++ } + + if (containerLog.isTraceEnabled()) { + containerLog.trace(" Found " + list.size() + " user internal roles"); +@@ -1961,16 +1879,23 @@ public class JNDIRealm extends RealmBase + } + + // Are we configured to do role searches? +- if ((connection.roleFormat == null) || (roleName == null)) ++ if ((connection.roleFormat == null) || (roleName == null)) { + return list; ++ } + +- // Set up parameters for an appropriate search +- String filter = connection.roleFormat.format(new String[] { doRFC2254Encoding(dn), username, userRoleId }); ++ // Set up parameters for an appropriate search filter ++ // The dn is already attribute value escaped but the others are not ++ // This is a filter so all input will require filter escaping ++ String filter = connection.roleFormat.format(new String[] { ++ doFilterEscaping(dn), ++ doFilterEscaping(doAttributeValueEscaping(username)), ++ doFilterEscaping(doAttributeValueEscaping(userRoleId)) }); + SearchControls controls = new SearchControls(); +- if (roleSubtree) ++ if (roleSubtree) { + controls.setSearchScope(SearchControls.SUBTREE_SCOPE); +- else ++ } else { + controls.setSearchScope(SearchControls.ONELEVEL_SCOPE); ++ } + controls.setReturningAttributes(new String[] {roleName}); + + String base = null; +@@ -1979,7 +1904,9 @@ public class JNDIRealm extends RealmBase + Name name = np.parse(dn); + String nameParts[] = new String[name.size()]; + for (int i = 0; i < name.size(); i++) { +- nameParts[i] = name.get(i); ++ // May have been returned with \ escaping rather than ++ // \. Make sure it is \. ++ nameParts[i] = convertToHexEscape(name.get(i)); + } + base = connection.roleBaseFormat.format(nameParts); + } else { +@@ -1990,25 +1917,28 @@ public class JNDIRealm extends RealmBase + NamingEnumeration results = searchAsUser(connection.context, user, base, filter, controls, + isRoleSearchAsUser()); + +- if (results == null) ++ if (results == null) { + return list; // Should never happen, but just in case ... ++ } + + Map groupMap = new HashMap<>(); + try { + while (results.hasMore()) { + SearchResult result = results.next(); + Attributes attrs = result.getAttributes(); +- if (attrs == null) ++ if (attrs == null) { + continue; +- String dname = getDistinguishedName(connection.context, roleBase, result); ++ } ++ String dname = getDistinguishedName(connection.context, base, result); + String name = getAttributeValue(roleName, attrs); + if (name != null && dname != null) { + groupMap.put(dname, name); + } + } + } catch (PartialResultException ex) { +- if (!adCompat) ++ if (!adCompat) { + throw ex; ++ } + } finally { + results.close(); + } +@@ -2033,22 +1963,28 @@ public class JNDIRealm extends RealmBase + Map newThisRound = new HashMap<>(); // Stores the groups we find in this iteration + + for (Entry group : newGroups.entrySet()) { +- filter = connection.roleFormat.format(new String[] { doRFC2254Encoding(group.getKey()), +- group.getValue(), group.getValue() }); ++ // Group key is already value escaped if required ++ // Group value is not value escaped ++ // Everything needs to be filter escaped ++ filter = connection.roleFormat.format(new String[] { ++ doFilterEscaping(group.getKey()), ++ doFilterEscaping(doAttributeValueEscaping(group.getValue())), ++ doFilterEscaping(doAttributeValueEscaping(group.getValue())) }); + + if (containerLog.isTraceEnabled()) { +- containerLog.trace("Perform a nested group search with base "+ roleBase + " and filter " + filter); ++ containerLog.trace("Perform a nested group search with base "+ roleBase + ++ " and filter " + filter); + } + +- results = searchAsUser(connection.context, user, roleBase, filter, controls, +- isRoleSearchAsUser()); ++ results = searchAsUser(connection.context, user, base, filter, controls, isRoleSearchAsUser()); + + try { + while (results.hasMore()) { + SearchResult result = results.next(); + Attributes attrs = result.getAttributes(); +- if (attrs == null) ++ if (attrs == null) { + continue; ++ } + String dname = getDistinguishedName(connection.context, roleBase, result); + String name = getAttributeValue(roleName, attrs); + if (name != null && dname != null && !groupMap.keySet().contains(dname)) { +@@ -2058,12 +1994,12 @@ public class JNDIRealm extends RealmBase + if (containerLog.isTraceEnabled()) { + containerLog.trace(" Found nested role " + dname + " -> " + name); + } +- + } +- } ++ } + } catch (PartialResultException ex) { +- if (!adCompat) ++ if (!adCompat) { + throw ex; ++ } + } finally { + results.close(); + } +@@ -2077,6 +2013,7 @@ public class JNDIRealm extends RealmBase + return list; + } + ++ + /** + * Perform the search on the context as the {@code dn}, when + * {@code searchAsUser} is {@code true}, otherwise search the context with +@@ -2099,8 +2036,7 @@ public class JNDIRealm extends RealmBase + * @throws NamingException + * if a directory server error occurs + */ +- private NamingEnumeration searchAsUser(DirContext context, +- User user, String base, String filter, ++ private NamingEnumeration searchAsUser(DirContext context, User user, String base, String filter, + SearchControls controls, boolean searchAsUser) throws NamingException { + NamingEnumeration results; + try { +@@ -2125,26 +2061,30 @@ public class JNDIRealm extends RealmBase + * @return the attribute value + * @exception NamingException if a directory server error occurs + */ +- private String getAttributeValue(String attrId, Attributes attrs) +- throws NamingException { ++ private String getAttributeValue(String attrId, Attributes attrs) throws NamingException { + +- if (containerLog.isTraceEnabled()) ++ if (containerLog.isTraceEnabled()) { + containerLog.trace(" retrieving attribute " + attrId); ++ } + +- if (attrId == null || attrs == null) ++ if (attrId == null || attrs == null) { + return null; ++ } + + Attribute attr = attrs.get(attrId); +- if (attr == null) ++ if (attr == null) { + return null; ++ } + Object value = attr.get(); +- if (value == null) ++ if (value == null) { + return null; ++ } + String valueString = null; +- if (value instanceof byte[]) ++ if (value instanceof byte[]) { + valueString = new String((byte[]) value); +- else ++ } else { + valueString = value.toString(); ++ } + + return valueString; + } +@@ -2159,20 +2099,22 @@ public class JNDIRealm extends RealmBase + * @return the list of attribute values + * @exception NamingException if a directory server error occurs + */ +- private ArrayList addAttributeValues(String attrId, +- Attributes attrs, +- ArrayList values) +- throws NamingException{ ++ private ArrayList addAttributeValues(String attrId, Attributes attrs, ArrayList values) ++ throws NamingException { + +- if (containerLog.isTraceEnabled()) ++ if (containerLog.isTraceEnabled()) { + containerLog.trace(" retrieving values for attribute " + attrId); +- if (attrId == null || attrs == null) ++ } ++ if (attrId == null || attrs == null) { + return values; +- if (values == null) ++ } ++ if (values == null) { + values = new ArrayList<>(); ++ } + Attribute attr = attrs.get(attrId); +- if (attr == null) ++ if (attr == null) { + return values; ++ } + NamingEnumeration e = attr.getAll(); + try { + while(e.hasMore()) { +@@ -2180,8 +2122,9 @@ public class JNDIRealm extends RealmBase + values.add(value); + } + } catch (PartialResultException ex) { +- if (!adCompat) ++ if (!adCompat) { + throw ex; ++ } + } finally { + e.close(); + } +@@ -2214,8 +2157,9 @@ public class JNDIRealm extends RealmBase + } + // Close our opened connection + try { +- if (containerLog.isDebugEnabled()) ++ if (containerLog.isDebugEnabled()) { + containerLog.debug("Closing directory context"); ++ } + connection.context.close(); + } catch (NamingException e) { + containerLog.error(sm.getString("jndiRealm.close"), e); +@@ -2225,9 +2169,9 @@ public class JNDIRealm extends RealmBase + if (connectionPool == null) { + singleConnectionLock.unlock(); + } +- + } + ++ + /** + * Close all pooled connections. + */ +@@ -2243,6 +2187,7 @@ public class JNDIRealm extends RealmBase + } + } + ++ + /** + * Get the password for the specified user. + * @param username The user name +@@ -2258,7 +2203,6 @@ public class JNDIRealm extends RealmBase + JNDIConnection connection = null; + User user = null; + try { +- + // Ensure that we have a directory context available + connection = get(); + +@@ -2281,7 +2225,6 @@ public class JNDIRealm extends RealmBase + user = getUser(connection, username, null); + } + +- + // Release this context + release(connection); + +@@ -2292,15 +2235,14 @@ public class JNDIRealm extends RealmBase + // ... and have a password + return user.getPassword(); + } +- + } catch (NamingException e) { + // Log the problem for posterity + containerLog.error(sm.getString("jndiRealm.exception"), e); + return null; + } +- + } + ++ + /** + * Get the principal associated with the specified certificate. + * @param username The user name +@@ -2311,9 +2253,9 @@ public class JNDIRealm extends RealmBase + return getPrincipal(username, null); + } + ++ + @Override +- protected Principal getPrincipal(GSSName gssName, +- GSSCredential gssCredential) { ++ protected Principal getPrincipal(GSSName gssName, GSSCredential gssCredential) { + String name = gssName.toString(); + + if (isStripRealmForGss()) { +@@ -2327,15 +2269,14 @@ public class JNDIRealm extends RealmBase + return getPrincipal(name, gssCredential); + } + ++ + @Override +- protected Principal getPrincipal(String username, +- GSSCredential gssCredential) { ++ protected Principal getPrincipal(String username, GSSCredential gssCredential) { + + JNDIConnection connection = null; + Principal principal = null; + + try { +- + // Ensure that we have a directory context available + connection = get(); + +@@ -2347,7 +2288,6 @@ public class JNDIRealm extends RealmBase + principal = getPrincipal(connection, username, gssCredential); + + } catch (CommunicationException | ServiceUnavailableException e) { +- + // log the exception so we know it's there. + containerLog.info(sm.getString("jndiRealm.exception.retry"), e); + +@@ -2360,10 +2300,8 @@ public class JNDIRealm extends RealmBase + + // Try the authentication again. + principal = getPrincipal(connection, username, gssCredential); +- + } + +- + // Release this context + release(connection); + +@@ -2371,16 +2309,12 @@ public class JNDIRealm extends RealmBase + return principal; + + } catch (NamingException e) { +- + // Log the problem for posterity + containerLog.error(sm.getString("jndiRealm.exception"), e); + + // Return "not authenticated" for this request + return null; +- + } +- +- + } + + +@@ -2392,9 +2326,8 @@ public class JNDIRealm extends RealmBase + * @return the Principal associated with the given certificate. + * @exception NamingException if a directory server error occurs + */ +- protected Principal getPrincipal(JNDIConnection connection, +- String username, GSSCredential gssCredential) +- throws NamingException { ++ protected Principal getPrincipal(JNDIConnection connection, String username, GSSCredential gssCredential) ++ throws NamingException { + + User user = null; + List roles = null; +@@ -2406,12 +2339,9 @@ public class JNDIRealm extends RealmBase + // Preserve the current context environment parameters + preservedEnvironment = context.getEnvironment(); + // Set up context +- context.addToEnvironment( +- Context.SECURITY_AUTHENTICATION, "GSSAPI"); +- context.addToEnvironment( +- "javax.security.sasl.server.authentication", "true"); +- context.addToEnvironment( +- "javax.security.sasl.qop", spnegoDelegationQop); ++ context.addToEnvironment(Context.SECURITY_AUTHENTICATION, "GSSAPI"); ++ context.addToEnvironment("javax.security.sasl.server.authentication", "true"); ++ context.addToEnvironment("javax.security.sasl.qop", spnegoDelegationQop); + // Note: Subject already set in SPNEGO authenticator so no need + // for Subject.doAs() here + } +@@ -2421,23 +2351,20 @@ public class JNDIRealm extends RealmBase + } + } finally { + if (gssCredential != null && isUseDelegatedCredential()) { +- restoreEnvironmentParameter(context, +- Context.SECURITY_AUTHENTICATION, preservedEnvironment); +- restoreEnvironmentParameter(context, +- "javax.security.sasl.server.authentication", preservedEnvironment); +- restoreEnvironmentParameter(context, "javax.security.sasl.qop", +- preservedEnvironment); ++ restoreEnvironmentParameter(context, Context.SECURITY_AUTHENTICATION, preservedEnvironment); ++ restoreEnvironmentParameter(context, "javax.security.sasl.server.authentication", preservedEnvironment); ++ restoreEnvironmentParameter(context, "javax.security.sasl.qop", preservedEnvironment); + } + } + + if (user != null) { +- return new GenericPrincipal(user.getUserName(), user.getPassword(), +- roles, null, null, gssCredential); ++ return new GenericPrincipal(user.getUserName(), user.getPassword(), roles, null, null, gssCredential); + } + + return null; + } + ++ + private void restoreEnvironmentParameter(DirContext context, + String parameterName, Hashtable preservedEnvironment) { + try { +@@ -2451,6 +2378,7 @@ public class JNDIRealm extends RealmBase + } + } + ++ + /** + * Open (if necessary) and return a connection to the configured + * directory server for this Realm. +@@ -2458,12 +2386,28 @@ public class JNDIRealm extends RealmBase + * @exception NamingException if a directory server error occurs + */ + protected JNDIConnection get() throws NamingException { ++ return get(false); ++ } ++ ++ /** ++ * Open (if necessary) and return a connection to the configured ++ * directory server for this Realm. ++ * @param create when pooling, this forces creation of a new connection, ++ * for example after an error ++ * @return the connection ++ * @exception NamingException if a directory server error occurs ++ */ ++ protected JNDIConnection get(boolean create) throws NamingException { + JNDIConnection connection = null; + // Use the pool if available, otherwise use the single connection + if (connectionPool != null) { +- connection = connectionPool.pop(); +- if (connection == null) { ++ if (create) { + connection = create(); ++ } else { ++ connection = connectionPool.pop(); ++ if (connection == null) { ++ connection = create(); ++ } + } + } else { + singleConnectionLock.lock(); +@@ -2475,6 +2419,7 @@ public class JNDIRealm extends RealmBase + return connection; + } + ++ + /** + * Release our use of this connection so that it can be recycled. + * +@@ -2491,6 +2436,7 @@ public class JNDIRealm extends RealmBase + } + } + ++ + /** + * Create a new connection wrapper, along with the + * message formats. +@@ -2505,8 +2451,7 @@ public class JNDIRealm extends RealmBase + int len = userPatternArray.length; + connection.userPatternFormatArray = new MessageFormat[len]; + for (int i = 0; i < len; i++) { +- connection.userPatternFormatArray[i] = +- new MessageFormat(userPatternArray[i]); ++ connection.userPatternFormatArray[i] = new MessageFormat(userPatternArray[i]); + } + } + if (roleBase != null) { +@@ -2518,6 +2463,7 @@ public class JNDIRealm extends RealmBase + return connection; + } + ++ + /** + * Create a new connection to the directory server. + * @param connection The directory server connection wrapper +@@ -2552,12 +2498,14 @@ public class JNDIRealm extends RealmBase + } + } + ++ + @Override + public boolean isAvailable() { + // Simple best effort check + return (connectionPool != null || singleConnection.context != null); + } + ++ + private DirContext createDirContext(Hashtable env) throws NamingException { + if (useStartTls) { + return createTlsDirContext(env); +@@ -2566,13 +2514,13 @@ public class JNDIRealm extends RealmBase + } + } + ++ + private SSLSocketFactory getSSLSocketFactory() { + if (sslSocketFactory != null) { + return sslSocketFactory; + } + final SSLSocketFactory result; +- if (this.sslSocketFactoryClassName != null +- && !sslSocketFactoryClassName.trim().equals("")) { ++ if (this.sslSocketFactoryClassName != null && !sslSocketFactoryClassName.trim().equals("")) { + result = createSSLSocketFactoryFromClassName(this.sslSocketFactoryClassName); + } else { + result = createSSLContextFactoryFromProtocol(sslProtocol); +@@ -2581,6 +2529,7 @@ public class JNDIRealm extends RealmBase + return result; + } + ++ + private SSLSocketFactory createSSLSocketFactoryFromClassName(String className) { + try { + Object o = constructInstance(className); +@@ -2598,6 +2547,7 @@ public class JNDIRealm extends RealmBase + } + } + ++ + private SSLSocketFactory createSSLContextFactoryFromProtocol(String protocol) { + try { + SSLContext sslContext; +@@ -2609,14 +2559,13 @@ public class JNDIRealm extends RealmBase + } + return sslContext.getSocketFactory(); + } catch (NoSuchAlgorithmException | KeyManagementException e) { +- List allowedProtocols = Arrays +- .asList(getSupportedSslProtocols()); +- throw new IllegalArgumentException( +- sm.getString("jndiRealm.invalidSslProtocol", protocol, +- allowedProtocols), e); ++ List allowedProtocols = Arrays.asList(getSupportedSslProtocols()); ++ throw new IllegalArgumentException(sm.getString("jndiRealm.invalidSslProtocol", ++ protocol, allowedProtocols), e); + } + } + ++ + /** + * Create a tls enabled LdapContext and set the StartTlsResponse tls + * instance variable. +@@ -2627,12 +2576,10 @@ public class JNDIRealm extends RealmBase + * @throws NamingException + * when something goes wrong while negotiating the connection + */ +- private DirContext createTlsDirContext( +- Hashtable env) throws NamingException { ++ private DirContext createTlsDirContext(Hashtable env) throws NamingException { + Map savedEnv = new HashMap<>(); +- for (String key : Arrays.asList(Context.SECURITY_AUTHENTICATION, +- Context.SECURITY_CREDENTIALS, Context.SECURITY_PRINCIPAL, +- Context.SECURITY_PROTOCOL)) { ++ for (String key : Arrays.asList(Context.SECURITY_AUTHENTICATION, Context.SECURITY_CREDENTIALS, ++ Context.SECURITY_PRINCIPAL, Context.SECURITY_PROTOCOL)) { + Object entry = env.remove(key); + if (entry != null) { + savedEnv.put(key, entry); +@@ -2641,8 +2588,7 @@ public class JNDIRealm extends RealmBase + LdapContext result = null; + try { + result = new InitialLdapContext(env, null); +- tls = (StartTlsResponse) result +- .extendedOperation(new StartTlsRequest()); ++ tls = (StartTlsResponse) result.extendedOperation(new StartTlsRequest()); + if (getHostnameVerifier() != null) { + tls.setHostnameVerifier(getHostnameVerifier()); + } +@@ -2651,22 +2597,21 @@ public class JNDIRealm extends RealmBase + } + try { + SSLSession negotiate = tls.negotiate(getSSLSocketFactory()); +- containerLog.debug(sm.getString("jndiRealm.negotiatedTls", +- negotiate.getProtocol())); ++ containerLog.debug(sm.getString("jndiRealm.negotiatedTls", negotiate.getProtocol())); + } catch (IOException e) { + throw new NamingException(e.getMessage()); + } + } finally { + if (result != null) { + for (Map.Entry savedEntry : savedEnv.entrySet()) { +- result.addToEnvironment(savedEntry.getKey(), +- savedEntry.getValue()); ++ result.addToEnvironment(savedEntry.getKey(), savedEntry.getValue()); + } + } + } + return result; + } + ++ + /** + * Create our directory context configuration. + * +@@ -2677,40 +2622,48 @@ public class JNDIRealm extends RealmBase + Hashtable env = new Hashtable<>(); + + // Configure our directory context environment. +- if (containerLog.isDebugEnabled() && connectionAttempt == 0) ++ if (containerLog.isDebugEnabled() && connectionAttempt == 0) { + containerLog.debug("Connecting to URL " + connectionURL); +- else if (containerLog.isDebugEnabled() && connectionAttempt > 0) ++ } else if (containerLog.isDebugEnabled() && connectionAttempt > 0) { + containerLog.debug("Connecting to URL " + alternateURL); ++ } + env.put(Context.INITIAL_CONTEXT_FACTORY, contextFactory); +- if (connectionName != null) ++ if (connectionName != null) { + env.put(Context.SECURITY_PRINCIPAL, connectionName); +- if (connectionPassword != null) ++ } ++ if (connectionPassword != null) { + env.put(Context.SECURITY_CREDENTIALS, connectionPassword); +- if (connectionURL != null && connectionAttempt == 0) ++ } ++ if (connectionURL != null && connectionAttempt == 0) { + env.put(Context.PROVIDER_URL, connectionURL); +- else if (alternateURL != null && connectionAttempt > 0) ++ } else if (alternateURL != null && connectionAttempt > 0) { + env.put(Context.PROVIDER_URL, alternateURL); +- if (authentication != null) ++ } ++ if (authentication != null) { + env.put(Context.SECURITY_AUTHENTICATION, authentication); +- if (protocol != null) ++ } ++ if (protocol != null) { + env.put(Context.SECURITY_PROTOCOL, protocol); +- if (referrals != null) ++ } ++ if (referrals != null) { + env.put(Context.REFERRAL, referrals); +- if (derefAliases != null) ++ } ++ if (derefAliases != null) { + env.put(JNDIRealm.DEREF_ALIASES, derefAliases); +- if (connectionTimeout != null) ++ } ++ if (connectionTimeout != null) { + env.put("com.sun.jndi.ldap.connect.timeout", connectionTimeout); +- if (readTimeout != null) ++ } ++ if (readTimeout != null) { + env.put("com.sun.jndi.ldap.read.timeout", readTimeout); ++ } + + return env; +- + } + + + // ------------------------------------------------------ Lifecycle Methods + +- + /** + * Prepare for the beginning of active use of the public methods of this + * component and implement the requirements of +@@ -2752,7 +2705,7 @@ public class JNDIRealm extends RealmBase + * @exception LifecycleException if this component detects a fatal error + * that needs to be reported + */ +- @Override ++ @Override + protected void stopInternal() throws LifecycleException { + super.stopInternal(); + // Close any open directory server connection +@@ -2765,6 +2718,7 @@ public class JNDIRealm extends RealmBase + } + } + ++ + /** + * Given a string containing LDAP patterns for user locations (separated by + * parentheses in a pseudo-LDAP search string format - +@@ -2799,8 +2753,7 @@ public class JNDIRealm extends RealmBase + while (userPatternString.charAt(endParenLoc - 1) == '\\') { + endParenLoc = userPatternString.indexOf(')', endParenLoc+1); + } +- String nextPathPart = userPatternString.substring +- (startParenLoc+1, endParenLoc); ++ String nextPathPart = userPatternString.substring(startParenLoc+1, endParenLoc); + pathList.add(nextPathPart); + startingPoint = endParenLoc+1; + startParenLoc = userPatternString.indexOf('(', startingPoint); +@@ -2808,7 +2761,6 @@ public class JNDIRealm extends RealmBase + return pathList.toArray(new String[] {}); + } + return null; +- + } + + +@@ -2823,10 +2775,36 @@ public class JNDIRealm extends RealmBase + * ) -> \29 + * \ -> \5c + * \0 -> \00 ++ * + * @param inString string to escape according to RFC 2254 guidelines ++ * + * @return String the escaped/encoded result ++ * ++ * @deprecated Will be removed in Tomcat 10.1.x onwards + */ ++ @Deprecated + protected String doRFC2254Encoding(String inString) { ++ return doFilterEscaping(inString); ++ } ++ ++ ++ /** ++ * Given an LDAP search string, returns the string with certain characters ++ * escaped according to RFC 2254 guidelines. ++ * The character mapping is as follows: ++ * char -> Replacement ++ * --------------------------- ++ * * -> \2a ++ * ( -> \28 ++ * ) -> \29 ++ * \ -> \5c ++ * \0 -> \00 ++ * ++ * @param inString string to escape according to RFC 2254 guidelines ++ * ++ * @return String the escaped/encoded result ++ */ ++ protected String doFilterEscaping(String inString) { + StringBuilder buf = new StringBuilder(inString.length()); + for (int i = 0; i < inString.length(); i++) { + char c = inString.charAt(i); +@@ -2864,47 +2842,42 @@ public class JNDIRealm extends RealmBase + * @return String containing the distinguished name + * @exception NamingException if a directory server error occurs + */ +- protected String getDistinguishedName(DirContext context, String base, +- SearchResult result) throws NamingException { ++ protected String getDistinguishedName(DirContext context, String base, SearchResult result) throws NamingException { + // Get the entry's distinguished name. For relative results, this means + // we need to composite a name with the base name, the context name, and + // the result name. For non-relative names, use the returned name. + String resultName = result.getName(); + Name name; + if (result.isRelative()) { +- if (containerLog.isTraceEnabled()) { +- containerLog.trace(" search returned relative name: " + resultName); +- } +- NameParser parser = context.getNameParser(""); +- Name contextName = parser.parse(context.getNameInNamespace()); +- Name baseName = parser.parse(base); ++ if (containerLog.isTraceEnabled()) { ++ containerLog.trace(" search returned relative name: " + resultName); ++ } ++ NameParser parser = context.getNameParser(""); ++ Name contextName = parser.parse(context.getNameInNamespace()); ++ Name baseName = parser.parse(base); + +- // Bugzilla 32269 +- Name entryName = parser.parse(new CompositeName(resultName).get(0)); ++ // Bugzilla 32269 ++ Name entryName = parser.parse(new CompositeName(resultName).get(0)); + +- name = contextName.addAll(baseName); +- name = name.addAll(entryName); ++ name = contextName.addAll(baseName); ++ name = name.addAll(entryName); + } else { +- if (containerLog.isTraceEnabled()) { +- containerLog.trace(" search returned absolute name: " + resultName); +- } +- try { +- // Normalize the name by running it through the name parser. +- NameParser parser = context.getNameParser(""); +- URI userNameUri = new URI(resultName); +- String pathComponent = userNameUri.getPath(); +- // Should not ever have an empty path component, since that is /{DN} +- if (pathComponent.length() < 1 ) { +- throw new InvalidNameException( +- "Search returned unparseable absolute name: " + +- resultName ); +- } +- name = parser.parse(pathComponent.substring(1)); +- } catch ( URISyntaxException e ) { +- throw new InvalidNameException( +- "Search returned unparseable absolute name: " + +- resultName ); +- } ++ if (containerLog.isTraceEnabled()) { ++ containerLog.trace(" search returned absolute name: " + resultName); ++ } ++ try { ++ // Normalize the name by running it through the name parser. ++ NameParser parser = context.getNameParser(""); ++ URI userNameUri = new URI(resultName); ++ String pathComponent = userNameUri.getPath(); ++ // Should not ever have an empty path component, since that is /{DN} ++ if (pathComponent.length() < 1 ) { ++ throw new InvalidNameException("Search returned unparseable absolute name: " + resultName); ++ } ++ name = parser.parse(pathComponent.substring(1)); ++ } catch ( URISyntaxException e ) { ++ throw new InvalidNameException("Search returned unparseable absolute name: " + resultName); ++ } + } + + if (getForceDnHexEscape()) { +@@ -2916,6 +2889,78 @@ public class JNDIRealm extends RealmBase + } + + ++ /** ++ * Implements the necessary escaping to represent an attribute value as a ++ * String as per RFC 4514. ++ * ++ * @param input The original attribute value ++ * @return The string representation of the attribute value ++ */ ++ protected String doAttributeValueEscaping(String input) { ++ int len = input.length(); ++ StringBuilder result = new StringBuilder(); ++ ++ for (int i = 0; i < len; i++) { ++ char c = input.charAt(i); ++ switch (c) { ++ case ' ': { ++ if (i == 0 || i == (len -1)) { ++ result.append("\\20"); ++ } else { ++ result.append(c); ++ } ++ break; ++ } ++ case '#': { ++ if (i == 0 ) { ++ result.append("\\23"); ++ } else { ++ result.append(c); ++ } ++ break; ++ } ++ case '\"': { ++ result.append("\\22"); ++ break; ++ } ++ case '+': { ++ result.append("\\2B"); ++ break; ++ } ++ case ',': { ++ result.append("\\2C"); ++ break; ++ } ++ case ';': { ++ result.append("\\3B"); ++ break; ++ } ++ case '<': { ++ result.append("\\3C"); ++ break; ++ } ++ case '>': { ++ result.append("\\3E"); ++ break; ++ } ++ case '\\': { ++ result.append("\\5C"); ++ break; ++ } ++ case '\u0000': { ++ result.append("\\00"); ++ break; ++ } ++ default: ++ result.append(c); ++ } ++ ++ } ++ ++ return result.toString(); ++ } ++ ++ + protected static String convertToHexEscape(String input) { + if (input.indexOf('\\') == -1) { + // No escaping present. Return original. +@@ -2992,7 +3037,7 @@ public class JNDIRealm extends RealmBase + } + + +- // ------------------------------------------------------ Private Classes ++ // ------------------------------------------------------ Protected Classes + + /** + * A protected class representing a User +@@ -3005,9 +3050,7 @@ public class JNDIRealm extends RealmBase + private final List roles; + private final String userRoleId; + +- +- public User(String username, String dn, String password, +- List roles, String userRoleId) { ++ public User(String username, String dn, String password, List roles, String userRoleId) { + this.username = username; + this.dn = dn; + this.password = password; +@@ -3040,6 +3083,7 @@ public class JNDIRealm extends RealmBase + } + } + ++ + /** + * Class holding the connection to the directory plus the associated + * non thread safe message formats. +@@ -3074,8 +3118,5 @@ public class JNDIRealm extends RealmBase + * The directory context linking us to our directory server. + */ + protected DirContext context = null; +- + } +- + } +- +Index: apache-tomcat-9.0.43-src/test/org/apache/catalina/realm/TestJNDIRealmAttributeValueEscape.java +=================================================================== +--- /dev/null ++++ apache-tomcat-9.0.43-src/test/org/apache/catalina/realm/TestJNDIRealmAttributeValueEscape.java +@@ -0,0 +1,88 @@ ++ ++@@ -0,0 +1,86 @@ ++/* ++ * Licensed to the Apache Software Foundation (ASF) under one or more ++ * contributor license agreements. See the NOTICE file distributed with ++ * this work for additional information regarding copyright ownership. ++ * The ASF licenses this file to You under the Apache License, Version 2.0 ++ * (the "License"); you may not use this file except in compliance with ++ * the License. You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, software ++ * distributed under the License is distributed on an "AS IS" BASIS, ++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++ * See the License for the specific language governing permissions and ++ * limitations under the License. ++ */ ++package org.apache.catalina.realm; ++ ++import java.util.ArrayList; ++import java.util.Collection; ++import java.util.List; ++ ++import org.junit.Assert; ++import org.junit.Test; ++import org.junit.runner.RunWith; ++import org.junit.runners.Parameterized; ++import org.junit.runners.Parameterized.Parameter; ++ ++@RunWith(Parameterized.class) ++public class TestJNDIRealmAttributeValueEscape { ++ ++ @Parameterized.Parameters(name = "{index}: in[{0}], out[{1}]") ++ public static Collection parameters() { ++ List parameterSets = new ArrayList<>(); ++ ++ // No escaping required ++ parameterSets.add(new String[] { "none", "none" }); ++ // Simple cases (same order as RFC 4512 section 2) ++ // Each appearing at the beginning, middle and ent ++ parameterSets.add(new String[] { " test", "\\20test" }); ++ parameterSets.add(new String[] { "te st", "te st" }); ++ parameterSets.add(new String[] { "test ", "test\\20" }); ++ parameterSets.add(new String[] { "#test", "\\23test" }); ++ parameterSets.add(new String[] { "te#st", "te#st" }); ++ parameterSets.add(new String[] { "test#", "test#" }); ++ parameterSets.add(new String[] { "\"test", "\\22test" }); ++ parameterSets.add(new String[] { "te\"st", "te\\22st" }); ++ parameterSets.add(new String[] { "test\"", "test\\22" }); ++ parameterSets.add(new String[] { "+test", "\\2Btest" }); ++ parameterSets.add(new String[] { "te+st", "te\\2Bst" }); ++ parameterSets.add(new String[] { "test+", "test\\2B" }); ++ parameterSets.add(new String[] { ",test", "\\2Ctest" }); ++ parameterSets.add(new String[] { "te,st", "te\\2Cst" }); ++ parameterSets.add(new String[] { "test,", "test\\2C" }); ++ parameterSets.add(new String[] { ";test", "\\3Btest" }); ++ parameterSets.add(new String[] { "te;st", "te\\3Bst" }); ++ parameterSets.add(new String[] { "test;", "test\\3B" }); ++ parameterSets.add(new String[] { "test", "\\3Etest" }); ++ parameterSets.add(new String[] { "te>st", "te\\3Est" }); ++ parameterSets.add(new String[] { "test>", "test\\3E" }); ++ parameterSets.add(new String[] { "\\test", "\\5Ctest" }); ++ parameterSets.add(new String[] { "te\\st", "te\\5Cst" }); ++ parameterSets.add(new String[] { "test\\", "test\\5C" }); ++ parameterSets.add(new String[] { "\u0000test", "\\00test" }); ++ parameterSets.add(new String[] { "te\u0000st", "te\\00st" }); ++ parameterSets.add(new String[] { "test\u0000", "test\\00" }); ++ return parameterSets; ++ } ++ ++ ++ @Parameter(0) ++ public String in; ++ @Parameter(1) ++ public String out; ++ ++ private JNDIRealm realm = new JNDIRealm(); ++ ++ @Test ++ public void testConvertToHexEscape() throws Exception { ++ String result = realm.doAttributeValueEscaping(in); ++ Assert.assertEquals(out, result); ++ } ++} +Index: apache-tomcat-9.0.43-src/test/org/apache/catalina/realm/TestJNDIRealmIntegration.java +=================================================================== +--- /dev/null ++++ apache-tomcat-9.0.43-src/test/org/apache/catalina/realm/TestJNDIRealmIntegration.java +@@ -0,0 +1,263 @@ ++/* ++ * Licensed to the Apache Software Foundation (ASF) under one or more ++ * contributor license agreements. See the NOTICE file distributed with ++ * this work for additional information regarding copyright ownership. ++ * The ASF licenses this file to You under the Apache License, Version 2.0 ++ * (the "License"); you may not use this file except in compliance with ++ * the License. You may obtain a copy of the License at ++ * ++ * http://www.apache.org/licenses/LICENSE-2.0 ++ * ++ * Unless required by applicable law or agreed to in writing, software ++ * distributed under the License is distributed on an "AS IS" BASIS, ++ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ++ * See the License for the specific language governing permissions and ++ * limitations under the License. ++ */ ++package org.apache.catalina.realm; ++ ++import java.util.ArrayList; ++import java.util.Arrays; ++import java.util.Collection; ++import java.util.HashSet; ++import java.util.List; ++import java.util.Set; ++ ++import org.junit.AfterClass; ++import org.junit.Assert; ++import org.junit.BeforeClass; ++import org.junit.Test; ++import org.junit.runner.RunWith; ++import org.junit.runners.Parameterized; ++import org.junit.runners.Parameterized.Parameter; ++ ++import org.apache.juli.logging.LogFactory; ++ ++import com.unboundid.ldap.listener.InMemoryDirectoryServer; ++import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; ++import com.unboundid.ldap.sdk.AddRequest; ++import com.unboundid.ldap.sdk.LDAPConnection; ++import com.unboundid.ldap.sdk.LDAPResult; ++import com.unboundid.ldap.sdk.ResultCode; ++ ++@RunWith(Parameterized.class) ++public class TestJNDIRealmIntegration { ++ ++ private static final String USER_PATTERN = "cn={0},ou=people,dc=example,dc=com"; ++ private static final String USER_SEARCH = "cn={0}"; ++ private static final String USER_BASE = "ou=people,dc=example,dc=com"; ++ private static final String ROLE_SEARCH_A = "member={0}"; ++ private static final String ROLE_SEARCH_B = "member=cn={1},ou=people,dc=example,dc=com"; ++ private static final String ROLE_SEARCH_C = "member=cn={2},ou=people,dc=example,dc=com"; ++ private static final String ROLE_BASE = "ou=people,dc=example,dc=com"; ++ ++ private static InMemoryDirectoryServer ldapServer; ++ ++ @Parameterized.Parameters(name = "{index}: user[{5}], pwd[{6}]") ++ public static Collection parameters() { ++ List parameterSets = new ArrayList<>(); ++ for (String roleSearch : new String[] { ROLE_SEARCH_A, ROLE_SEARCH_B, ROLE_SEARCH_C }) { ++ addUsers(USER_PATTERN, null, null, roleSearch, ROLE_BASE, parameterSets); ++ addUsers(null, USER_SEARCH, USER_BASE, roleSearch, ROLE_BASE, parameterSets); ++ } ++ parameterSets.add(new Object[] { "cn={0},ou=s\\;ub,ou=people,dc=example,dc=com", null, null, ROLE_SEARCH_A, ++ "{3},ou=people,dc=example,dc=com", "testsub", "test", new String[] {"TestGroup4"} }); ++ return parameterSets; ++ } ++ ++ ++ private static void addUsers(String userPattern, String userSearch, String userBase, String roleSearch, ++ String roleBase, List parameterSets) { ++ parameterSets.add(new Object[] { userPattern, userSearch, userBase, roleSearch, roleBase, ++ "test", "test", new String[] {"TestGroup"} }); ++ parameterSets.add(new Object[] { userPattern, userSearch, userBase, roleSearch, roleBase, ++ "t;", "test", new String[] {"TestGroup"} }); ++ parameterSets.add(new Object[] { userPattern, userSearch, userBase, roleSearch, roleBase, ++ "t*", "test", new String[] {"TestGroup"} }); ++ parameterSets.add(new Object[] { userPattern, userSearch, userBase, roleSearch, roleBase, ++ "t=", "test", new String[] {"TestGroup*3"} }); ++ } ++ ++ ++ @Parameter(0) ++ public String realmConfigUserPattern; ++ @Parameter(1) ++ public String realmConfigUserSearch; ++ @Parameter(2) ++ public String realmConfigUserBase; ++ @Parameter(3) ++ public String realmConfigRoleSearch; ++ @Parameter(4) ++ public String realmConfigRoleBase; ++ @Parameter(5) ++ public String username; ++ @Parameter(6) ++ public String credentials; ++ @Parameter(7) ++ public String[] groups; ++ ++ @Test ++ public void testAuthenication() throws Exception { ++ JNDIRealm realm = new JNDIRealm(); ++ realm.containerLog = LogFactory.getLog(TestJNDIRealmIntegration.class); ++ ++ realm.setConnectionURL("ldap://localhost:" + ldapServer.getListenPort()); ++ realm.setUserPattern(realmConfigUserPattern); ++ realm.setUserSearch(realmConfigUserSearch); ++ realm.setUserBase(realmConfigUserBase); ++ realm.setUserRoleAttribute("cn"); ++ realm.setRoleName("cn"); ++ realm.setRoleBase(realmConfigRoleBase); ++ realm.setRoleSearch(realmConfigRoleSearch); ++ realm.setRoleNested(true); ++ ++ GenericPrincipal p = (GenericPrincipal) realm.authenticate(username, credentials); ++ ++ Assert.assertNotNull(p); ++ Assert.assertEquals(username, p.name); ++ ++ Set actualGroups = new HashSet<>(Arrays.asList(p.getRoles())); ++ Set expectedGroups = new HashSet<>(Arrays.asList(groups)); ++ ++ Assert.assertEquals(expectedGroups.size(), actualGroups.size()); ++ Set tmp = new HashSet<>(); ++ tmp.addAll(expectedGroups); ++ tmp.removeAll(actualGroups); ++ Assert.assertEquals(0, tmp.size()); ++ } ++ ++ ++ @BeforeClass ++ public static void createLDAP() throws Exception { ++ InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=example,dc=com"); ++ config.addAdditionalBindCredentials("cn=admin", "password"); ++ ldapServer = new InMemoryDirectoryServer(config); ++ ++ ldapServer.startListening(); ++ ++ try (LDAPConnection conn = ldapServer.getConnection()) { ++ ++ // Note: Only the DNs need attribute value escaping ++ AddRequest addBase = new AddRequest( ++ "dn: dc=example,dc=com", ++ "objectClass: top", ++ "objectClass: domain", ++ "dc: example"); ++ LDAPResult result = conn.processOperation(addBase); ++ Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode()); ++ ++ AddRequest addPeople = new AddRequest( ++ "dn: ou=people,dc=example,dc=com", ++ "objectClass: top", ++ "objectClass: organizationalUnit"); ++ result = conn.processOperation(addPeople); ++ Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode()); ++ ++ AddRequest addUserTest = new AddRequest( ++ "dn: cn=test,ou=people,dc=example,dc=com", ++ "objectClass: top", ++ "objectClass: person", ++ "objectClass: organizationalPerson", ++ "cn: test", ++ "sn: Test", ++ "userPassword: test"); ++ result = conn.processOperation(addUserTest); ++ Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode()); ++ ++ AddRequest addUserTestSemicolon = new AddRequest( ++ "dn: cn=t\\;,ou=people,dc=example,dc=com", ++ "objectClass: top", ++ "objectClass: person", ++ "objectClass: organizationalPerson", ++ "cn: t;", ++ "sn: Tsemicolon", ++ "userPassword: test"); ++ result = conn.processOperation(addUserTestSemicolon); ++ Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode()); ++ ++ AddRequest addUserTestAsterisk = new AddRequest( ++ "dn: cn=t*,ou=people,dc=example,dc=com", ++ "objectClass: top", ++ "objectClass: person", ++ "objectClass: organizationalPerson", ++ "cn: t*", ++ "sn: Tasterisk", ++ "userPassword: test"); ++ result = conn.processOperation(addUserTestAsterisk); ++ Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode()); ++ ++ AddRequest addUserTestEquals = new AddRequest( ++ "dn: cn=t\\=,ou=people,dc=example,dc=com", ++ "objectClass: top", ++ "objectClass: person", ++ "objectClass: organizationalPerson", ++ "cn: t=", ++ "sn: Tequals", ++ "userPassword: test"); ++ result = conn.processOperation(addUserTestEquals); ++ Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode()); ++ ++ AddRequest addGroupTest = new AddRequest( ++ "dn: cn=TestGroup,ou=people,dc=example,dc=com", ++ "objectClass: top", ++ "objectClass: groupOfNames", ++ "cn: TestGroup", ++ "member: cn=test,ou=people,dc=example,dc=com", ++ "member: cn=t\\;,ou=people,dc=example,dc=com", ++ "member: cn=t\\*,ou=people,dc=example,dc=com"); ++ result = conn.processOperation(addGroupTest); ++ Assert.assertEquals(ResultCode.SUCCESS, result.getResultCode()); ++ ++ AddRequest addGroupTest2 = new AddRequest( ++ "dn: cn=Test\\Group*3,ou=people,dc=example,dc=com", ++ "objectClass: top", ++ "objectClass: groupOfNames", ++ "cn: Test>Group*3", ++ "member: cn=Test\\65106: Fix the ConfigFileLoader handling of file URIs when + running under a security manager on some JREs. (markt) + ++ ++ Expand coverage of unit tests for JNDIRealm using the UnboundID LDAP SDK ++ for Java. (markt) ++ + + + +@@ -576,6 +580,11 @@ + JRE bug. + (markt) + ++ ++ Fix JNDIRealm pooling problems retrying on another bad connection. Any ++ retries are made on a new connection, just like with the single ++ connection scenario. (remm) ++ + + + diff --git a/tomcat-9.0-CVE-2021-33037.patch b/tomcat-9.0-CVE-2021-33037.patch new file mode 100644 index 0000000..193d404 --- /dev/null +++ b/tomcat-9.0-CVE-2021-33037.patch @@ -0,0 +1,195 @@ +Index: apache-tomcat-9.0.43-src/java/org/apache/coyote/http11/Http11Processor.java +=================================================================== +--- apache-tomcat-9.0.43-src.orig/java/org/apache/coyote/http11/Http11Processor.java ++++ apache-tomcat-9.0.43-src/java/org/apache/coyote/http11/Http11Processor.java +@@ -212,11 +212,8 @@ public class Http11Processor extends Abs + + // Parsing trims and converts to lower case. + +- if (encodingName.equals("identity")) { +- // Skip +- } else if (encodingName.equals("chunked")) { +- inputBuffer.addActiveFilter +- (inputFilters[Constants.CHUNKED_FILTER]); ++ if (encodingName.equals("chunked")) { ++ inputBuffer.addActiveFilter(inputFilters[Constants.CHUNKED_FILTER]); + contentDelimitation = true; + } else { + for (int i = pluggableFilterIndex; i < inputFilters.length; i++) { +@@ -753,13 +750,14 @@ public class Http11Processor extends Abs + InputFilter[] inputFilters = inputBuffer.getFilters(); + + // Parse transfer-encoding header +- if (http11) { ++ // HTTP specs say an HTTP 1.1 server should accept any recognised ++ // HTTP 1.x header from a 1.x client unless the specs says otherwise. ++ if (!http09) { + MessageBytes transferEncodingValueMB = headers.getValue("transfer-encoding"); + if (transferEncodingValueMB != null) { + List encodingNames = new ArrayList<>(); + if (TokenList.parseTokenList(headers.values("transfer-encoding"), encodingNames)) { + for (String encodingName : encodingNames) { +- // "identity" codings are ignored + addInputFilter(inputFilters, encodingName); + } + } else { +Index: apache-tomcat-9.0.43-src/test/org/apache/coyote/http11/TestHttp11Processor.java +=================================================================== +--- apache-tomcat-9.0.43-src.orig/test/org/apache/coyote/http11/TestHttp11Processor.java ++++ apache-tomcat-9.0.43-src/test/org/apache/coyote/http11/TestHttp11Processor.java +@@ -254,31 +254,6 @@ public class TestHttp11Processor extends + + + @Test +- public void testWithTEIdentity() throws Exception { +- getTomcatInstanceTestWebapp(false, true); +- +- String request = +- "POST /test/echo-params.jsp HTTP/1.1" + SimpleHttpClient.CRLF + +- "Host: any" + SimpleHttpClient.CRLF + +- "Transfer-encoding: identity" + SimpleHttpClient.CRLF + +- "Content-Length: 9" + SimpleHttpClient.CRLF + +- "Content-Type: application/x-www-form-urlencoded" + +- SimpleHttpClient.CRLF + +- "Connection: close" + SimpleHttpClient.CRLF + +- SimpleHttpClient.CRLF + +- "test=data"; +- +- Client client = new Client(getPort()); +- client.setRequest(new String[] {request}); +- +- client.connect(); +- client.processRequest(); +- Assert.assertTrue(client.isResponse200()); +- Assert.assertTrue(client.getResponseBody().contains("test - data")); +- } +- +- +- @Test + public void testWithTESavedRequest() throws Exception { + getTomcatInstanceTestWebapp(false, true); + +@@ -1859,4 +1834,102 @@ public class TestHttp11Processor extends + // NO-OP + } + } ++ ++ ++ @Test ++ public void testTEHeaderUnknown01() throws Exception { ++ doTestTEHeaderUnknown("identity"); ++ } ++ ++ ++ @Test ++ public void testTEHeaderUnknown02() throws Exception { ++ doTestTEHeaderUnknown("identity, chunked"); ++ } ++ ++ ++ @Test ++ public void testTEHeaderUnknown03() throws Exception { ++ doTestTEHeaderUnknown("unknown, chunked"); ++ } ++ ++ ++ @Test ++ public void testTEHeaderUnknown04() throws Exception { ++ doTestTEHeaderUnknown("void"); ++ } ++ ++ ++ @Test ++ public void testTEHeaderUnknown05() throws Exception { ++ doTestTEHeaderUnknown("void, chunked"); ++ } ++ ++ ++ @Test ++ public void testTEHeaderUnknown06() throws Exception { ++ doTestTEHeaderUnknown("void, identity"); ++ } ++ ++ ++ @Test ++ public void testTEHeaderUnknown07() throws Exception { ++ doTestTEHeaderUnknown("identity, void"); ++ } ++ ++ ++ private void doTestTEHeaderUnknown(String headerValue) throws Exception { ++ Tomcat tomcat = getTomcatInstance(); ++ ++ // No file system docBase required ++ Context ctx = tomcat.addContext("", null); ++ ++ // Add servlet ++ Tomcat.addServlet(ctx, "TesterServlet", new TesterServlet(false)); ++ ctx.addServletMappingDecoded("/foo", "TesterServlet"); ++ ++ tomcat.start(); ++ ++ String request = ++ "GET /foo HTTP/1.1" + SimpleHttpClient.CRLF + ++ "Host: localhost:" + getPort() + SimpleHttpClient.CRLF + ++ "Transfer-Encoding: " + headerValue + SimpleHttpClient.CRLF + ++ SimpleHttpClient.CRLF; ++ ++ Client client = new Client(tomcat.getConnector().getLocalPort()); ++ client.setRequest(new String[] {request}); ++ ++ client.connect(); ++ client.processRequest(false); ++ ++ Assert.assertTrue(client.isResponse501()); ++ } ++ ++ ++ @Test ++ public void testWithTEChunkedHttp10() throws Exception { ++ ++ getTomcatInstanceTestWebapp(false, true); ++ ++ String request = ++ "POST /test/echo-params.jsp HTTP/1.0" + SimpleHttpClient.CRLF + ++ "Host: any" + SimpleHttpClient.CRLF + ++ "Transfer-encoding: chunked" + SimpleHttpClient.CRLF + ++ "Content-Type: application/x-www-form-urlencoded" + ++ SimpleHttpClient.CRLF + ++ "Connection: close" + SimpleHttpClient.CRLF + ++ SimpleHttpClient.CRLF + ++ "9" + SimpleHttpClient.CRLF + ++ "test=data" + SimpleHttpClient.CRLF + ++ "0" + SimpleHttpClient.CRLF + ++ SimpleHttpClient.CRLF; ++ ++ Client client = new Client(getPort()); ++ client.setRequest(new String[] {request}); ++ ++ client.connect(); ++ client.processRequest(); ++ Assert.assertTrue(client.isResponse200()); ++ Assert.assertTrue(client.getResponseBody().contains("test - data")); ++ } + } +Index: apache-tomcat-9.0.43-src/webapps/docs/changelog.xml +=================================================================== +--- apache-tomcat-9.0.43-src.orig/webapps/docs/changelog.xml ++++ apache-tomcat-9.0.43-src/webapps/docs/changelog.xml +@@ -347,6 +347,16 @@ + connections are attempted and fail. Patch provided by Maurizio Adami. + (markt) + ++ ++ Remove support for the identity transfer encoding. The ++ inclusion of this encoding in RFC 2616 was an error that was corrected ++ in 2001. Requests using this transfer encoding will now receive a 501 ++ response. (markt) ++ ++ ++ Process transfer encoding headers from both HTTP 1.0 and HTTP 1.1 ++ clients. (markt) ++ + + + diff --git a/tomcat-9.0-CVE-2021-41079.patch b/tomcat-9.0-CVE-2021-41079.patch new file mode 100644 index 0000000..045c2e9 --- /dev/null +++ b/tomcat-9.0-CVE-2021-41079.patch @@ -0,0 +1,55 @@ +From d4b340fa8feaf55831f9a59350578f7b6ca048b8 Mon Sep 17 00:00:00 2001 +From: Mark Thomas +Date: Wed, 3 Mar 2021 12:00:46 +0000 +Subject: [PATCH] Improve robustness + +--- + .../apache/tomcat/util/net/openssl/LocalStrings.properties | 1 + + java/org/apache/tomcat/util/net/openssl/OpenSSLEngine.java | 6 ++++-- + webapps/docs/changelog.xml | 4 ++++ + 3 files changed, 9 insertions(+), 2 deletions(-) + +Index: apache-tomcat-9.0.43-src/java/org/apache/tomcat/util/net/openssl/LocalStrings.properties +=================================================================== +--- apache-tomcat-9.0.43-src.orig/java/org/apache/tomcat/util/net/openssl/LocalStrings.properties ++++ apache-tomcat-9.0.43-src/java/org/apache/tomcat/util/net/openssl/LocalStrings.properties +@@ -17,6 +17,7 @@ engine.ciphersFailure=Failed getting cip + engine.emptyCipherSuite=Empty cipher suite + engine.engineClosed=Engine is closed + engine.failedCipherSuite=Failed to enable cipher suite [{0}] ++engine.failedToReadAvailableBytes=There are plain text bytes available to read but no bytes were read + engine.inboundClose=Inbound closed before receiving peer's close_notify + engine.invalidBufferArray=offset: [{0}], length: [{1}] (expected: offset <= offset + length <= srcs.length [{2}]) + engine.invalidDestinationBuffersState=The state of the destination buffers changed concurrently while unwrapping bytes +Index: apache-tomcat-9.0.43-src/java/org/apache/tomcat/util/net/openssl/OpenSSLEngine.java +=================================================================== +--- apache-tomcat-9.0.43-src.orig/java/org/apache/tomcat/util/net/openssl/OpenSSLEngine.java ++++ apache-tomcat-9.0.43-src/java/org/apache/tomcat/util/net/openssl/OpenSSLEngine.java +@@ -592,8 +592,10 @@ public final class OpenSSLEngine extends + throw new SSLException(e); + } + +- if (bytesRead == 0) { +- break; ++ if (bytesRead <= 0) { ++ // This should not be possible. pendingApp is positive ++ // therefore the read should have read at least one byte. ++ throw new IllegalStateException(sm.getString("engine.failedToReadAvailableBytes")); + } + + bytesProduced += bytesRead; +Index: apache-tomcat-9.0.43-src/webapps/docs/changelog.xml +=================================================================== +--- apache-tomcat-9.0.43-src.orig/webapps/docs/changelog.xml ++++ apache-tomcat-9.0.43-src/webapps/docs/changelog.xml +@@ -173,6 +173,10 @@ + the access log file, include information on the current user in the + associated log message (markt) + ++ ++ Make handling of OpenSSL read errors more robust when plain text data is ++ reported to be available to read. (markt) ++ + + + diff --git a/tomcat-9.0-osgi-build.patch b/tomcat-9.0-osgi-build.patch index 0fec32a..1f2826c 100644 --- a/tomcat-9.0-osgi-build.patch +++ b/tomcat-9.0-osgi-build.patch @@ -2,10 +2,11 @@ Index: apache-tomcat-9.0.37-src/build.xml =================================================================== --- apache-tomcat-9.0.37-src.orig/build.xml +++ apache-tomcat-9.0.37-src/build.xml -@@ -3307,6 +3307,12 @@ Read the Building page on the Apache Tom +@@ -3307,6 +3307,13 @@ Read the Building page on the Apache Tom ++ + + + diff --git a/tomcat.changes b/tomcat.changes index 73eac81..22358af 100644 --- a/tomcat.changes +++ b/tomcat.changes @@ -1,3 +1,28 @@ +------------------------------------------------------------------- +Wed Nov 10 06:51:24 UTC 2021 - Fridrich Strba + +- Modified patch: + * tomcat-9.0-osgi-build.patch + + account for biz.aQute.bnd.ant artifact in aqute-bnd >= 5.2.0 + +------------------------------------------------------------------- +Fri Oct 29 11:15:32 UTC 2021 - Michele Bussolotto + +- Fixed CVEs: + * CVE-2021-30640: Escape parameters in JNDI Realm queries (bsc#1188279) + * CVE-2021-33037: Process T-E header from both HTTP 1.0 and HTTP 1.1. clients (bsc#1188278) +- Added patches: + * tomcat-9.0-CVE-2021-30640.patch + * tomcat-9.0-CVE-2021-33037.patch + +------------------------------------------------------------------- +Thu Oct 28 08:33:07 UTC 2021 - Michele Bussolotto + +- Fixed CVEs: + * CVE-2021-41079: Validate incoming TLS packet (bsc#1190558) +- Added patches: + * tomcat-9.0-CVE-2021-41079.patch + ------------------------------------------------------------------- Mon Oct 18 21:42:48 UTC 2021 - Marcel Witte diff --git a/tomcat.spec b/tomcat.spec index ed72c74..bf2c4cb 100644 --- a/tomcat.spec +++ b/tomcat.spec @@ -83,6 +83,9 @@ Patch4: tomcat-9.0-osgi-build.patch Patch5: tomcat-9.0.43-java8compat.patch # PATCH-FIX-OPENSUSE: set ajp connector secreteRequired to false by default to avoid tomcat not starting Patch6: tomcat-9.0.31-secretRequired-default.patch +Patch7: tomcat-9.0-CVE-2021-41079.patch +Patch8: tomcat-9.0-CVE-2021-33037.patch +Patch9: tomcat-9.0-CVE-2021-30640.patch BuildRequires: ant >= 1.8.1 BuildRequires: ant-antlr @@ -90,8 +93,8 @@ BuildRequires: apache-commons-collections BuildRequires: apache-commons-daemon BuildRequires: apache-commons-dbcp >= 2.0 BuildRequires: apache-commons-pool2 -BuildRequires: aqute-bnd >= 5.1.1 -BuildRequires: aqute-bndlib >= 5.1.1 +BuildRequires: aqute-bnd >= 5.2 +BuildRequires: aqute-bndlib >= 5.2 BuildRequires: ecj >= 4.4.0 BuildRequires: fdupes BuildRequires: findutils @@ -257,6 +260,9 @@ find . -type f \( -name "*.bat" -o -name "*.class" -o -name Thumbs.db -o -name " %patch4 -p1 %patch5 -p1 %patch6 -p1 +%patch7 -p1 +%patch8 -p1 +%patch9 -p1 # remove date from docs sed -i -e '/build-date/ d' webapps/docs/tomcat-docs.xsl @@ -293,6 +299,7 @@ ant -Dbase.path="." \ -Dwsdl4j-lib.jar="$(build-classpath wsdl4j)" \ -Dsaaj-api.jar="$(build-classpath geronimo-saaj-1.1-api)" \ -Dbnd.jar="$(build-classpath aqute-bnd/biz.aQute.bnd)" \ + -Dbndant.jar="$(build-classpath aqute-bnd/biz.aQute.bnd.ant)" \ -Dbndlib.jar="$(build-classpath aqute-bnd/biz.aQute.bndlib)" \ -Dbndlibg.jar="$(build-classpath aqute-bnd/aQute.libg)" \ -Dbndannotation.jar="$(build-classpath aqute-bnd/biz.aQute.bnd.annotation)" \