diff --git a/CVE-2021-30640-1.patch b/CVE-2021-30640-1.patch new file mode 100644 index 0000000000000000000000000000000000000000..e2f3b4f4b82afde079fea069f3ca6b8a7e372ea1 --- /dev/null +++ b/CVE-2021-30640-1.patch @@ -0,0 +1,204 @@ +From 6a719704236d3ce02100606290ff59b6a11f6b20 Mon Sep 17 00:00:00 2001 +From: Mark Thomas +Date: Tue, 13 Apr 2021 11:12:02 +0100 +Subject: [PATCH] Add attribute value escaping to support user names containing ';' + +--- + java/org/apache/catalina/realm/JNDIRealm.java | 79 ++++++++++++++++- + .../TestJNDIRealmAttributeValueEscape.java | 86 +++++++++++++++++++ + 2 files changed, 163 insertions(+), 2 deletions(-) + create mode 100644 test/org/apache/catalina/realm/TestJNDIRealmAttributeValueEscape.java + +diff --git a/java/org/apache/catalina/realm/JNDIRealm.java b/java/org/apache/catalina/realm/JNDIRealm.java +index 19fa704..54921dc 100644 +--- a/java/org/apache/catalina/realm/JNDIRealm.java ++++ b/java/org/apache/catalina/realm/JNDIRealm.java +@@ -1603,8 +1603,11 @@ public class JNDIRealm extends RealmBase { + 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); +@@ -2820,6 +2823,78 @@ System.out.println("userRoleName " + userRoleName + " " + attrs.get(userRoleName + } + + ++ /** ++ * 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. +diff --git a/test/org/apache/catalina/realm/TestJNDIRealmAttributeValueEscape.java b/test/org/apache/catalina/realm/TestJNDIRealmAttributeValueEscape.java +new file mode 100644 +index 0000000..677bcc5 +--- /dev/null ++++ b/test/org/apache/catalina/realm/TestJNDIRealmAttributeValueEscape.java +@@ -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); ++ } ++} +\ No newline at end of file +-- +2.23.0 + diff --git a/CVE-2021-30640-2.patch b/CVE-2021-30640-2.patch new file mode 100644 index 0000000000000000000000000000000000000000..de4cd44f388262f6493edbf2e320540b60815f19 --- /dev/null +++ b/CVE-2021-30640-2.patch @@ -0,0 +1,71 @@ +From f9a89674c08b55677424df7bd41685e72316e6bf Mon Sep 17 00:00:00 2001 +From: Mark Thomas +Date: Tue, 13 Apr 2021 11:35:07 +0100 +Subject: [PATCH] Rename for clarity + +--- + java/org/apache/catalina/realm/JNDIRealm.java | 30 +++++++++++++++++-- + 1 file changed, 28 insertions(+), 2 deletions(-) + +diff --git a/java/org/apache/catalina/realm/JNDIRealm.java b/java/org/apache/catalina/realm/JNDIRealm.java +index 54921dc..b60f393 100644 +--- a/java/org/apache/catalina/realm/JNDIRealm.java ++++ b/java/org/apache/catalina/realm/JNDIRealm.java +@@ -1942,7 +1942,7 @@ System.out.println("userRoleName " + userRoleName + " " + attrs.get(userRoleName + return list; + + // Set up parameters for an appropriate search +- String filter = connection.roleFormat.format(new String[] { doRFC2254Encoding(dn), username, userRoleId }); ++ String filter = connection.roleFormat.format(new String[] { doFilterEscaping(dn), username, userRoleId }); + SearchControls controls = new SearchControls(); + if (roleSubtree) + controls.setSearchScope(SearchControls.SUBTREE_SCOPE); +@@ -2010,7 +2010,7 @@ System.out.println("userRoleName " + userRoleName + " " + attrs.get(userRoleName + 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()), ++ filter = connection.roleFormat.format(new String[] { doFilterEscaping(group.getKey()), + group.getValue(), group.getValue() }); + + if (containerLog.isTraceEnabled()) { +@@ -2730,10 +2730,36 @@ System.out.println("userRoleName " + userRoleName + " " + attrs.get(userRoleName + * ) -> \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); +-- +2.23.0 + diff --git a/CVE-2021-30640-3.patch b/CVE-2021-30640-3.patch new file mode 100644 index 0000000000000000000000000000000000000000..ee5458f9590f419ff80ace63983ac64f748f0b8d --- /dev/null +++ b/CVE-2021-30640-3.patch @@ -0,0 +1,36 @@ +From 2e3924d0a8372ced148b42016432c038dd1ae487 Mon Sep 17 00:00:00 2001 +From: Mark Thomas +Date: Tue, 13 Apr 2021 11:43:51 +0100 +Subject: [PATCH] Expand tests and fix escaping issue when searching for users by filter + +--- + java/org/apache/catalina/realm/JNDIRealm.java | 6 +++++- + 1 file changed, 5 insertions(+), 1 deletion(-) + +diff --git a/java/org/apache/catalina/realm/JNDIRealm.java b/java/org/apache/catalina/realm/JNDIRealm.java +index b60f393..dcec473 100644 +--- a/java/org/apache/catalina/realm/JNDIRealm.java ++++ b/java/org/apache/catalina/realm/JNDIRealm.java +@@ -1648,7 +1648,9 @@ public class JNDIRealm extends RealmBase { + 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(); +@@ -1913,6 +1915,8 @@ System.out.println("userRoleName " + userRoleName + " " + attrs.get(userRoleName + if (user == null) + return null; + ++ // This is returned from the directory so will be attribute value ++ // escaped if required + String dn = user.getDN(); + String username = user.getUserName(); + String userRoleId = user.getUserRoleId(); +-- +2.23.0 + diff --git a/CVE-2021-30640-4.patch b/CVE-2021-30640-4.patch new file mode 100644 index 0000000000000000000000000000000000000000..d9a1d5112747b816a9f4b829eea8d3a810438dd1 --- /dev/null +++ b/CVE-2021-30640-4.patch @@ -0,0 +1,37 @@ +From 954eb10e9957055f60ee1e427baabfa32fc3d78b Mon Sep 17 00:00:00 2001 +From: Mark Thomas +Date: Tue, 13 Apr 2021 12:11:35 +0100 +Subject: [PATCH] Expand tests and fix an issue in escaping for group search + +--- + java/org/apache/catalina/realm/JNDIRealm.java | 7 ++++++- + 1 file changed, 6 insertions(+), 1 deletion(-) + +diff --git a/java/org/apache/catalina/realm/JNDIRealm.java b/java/org/apache/catalina/realm/JNDIRealm.java +index dcec473..1021ce8 100644 +--- a/java/org/apache/catalina/realm/JNDIRealm.java ++++ b/java/org/apache/catalina/realm/JNDIRealm.java +@@ -1918,6 +1918,8 @@ System.out.println("userRoleName " + userRoleName + " " + attrs.get(userRoleName + // 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(); + +@@ -1946,7 +1948,10 @@ System.out.println("userRoleName " + userRoleName + " " + attrs.get(userRoleName + return list; + + // Set up parameters for an appropriate search +- String filter = connection.roleFormat.format(new String[] { doFilterEscaping(dn), username, userRoleId }); ++ String filter = connection.roleFormat.format(new String[] { ++ doFilterEscaping(dn), ++ doFilterEscaping(doAttributeValueEscaping(username)), ++ userRoleId }); + SearchControls controls = new SearchControls(); + if (roleSubtree) + controls.setSearchScope(SearchControls.SUBTREE_SCOPE); +-- +2.23.0 + diff --git a/CVE-2021-30640-5.patch b/CVE-2021-30640-5.patch new file mode 100644 index 0000000000000000000000000000000000000000..f00fbb01dc02543b75b54a7b8387ab102a43f1e2 --- /dev/null +++ b/CVE-2021-30640-5.patch @@ -0,0 +1,32 @@ +From a13034d94c927286a7f4e17ab4f662727fbe6e9f Mon Sep 17 00:00:00 2001 +From: Mark Thomas +Date: Tue, 13 Apr 2021 12:20:06 +0100 +Subject: [PATCH] Expand tests and fix escaping issue in userRoleAttribute filter + +--- + java/org/apache/catalina/realm/JNDIRealm.java | 6 ++++-- + 1 file changed, 4 insertions(+), 2 deletions(-) + +diff --git a/java/org/apache/catalina/realm/JNDIRealm.java b/java/org/apache/catalina/realm/JNDIRealm.java +index 1021ce8..a3b6f86 100644 +--- a/java/org/apache/catalina/realm/JNDIRealm.java ++++ b/java/org/apache/catalina/realm/JNDIRealm.java +@@ -1947,11 +1947,13 @@ System.out.println("userRoleName " + userRoleName + " " + attrs.get(userRoleName + if ((connection.roleFormat == null) || (roleName == null)) + return list; + +- // Set up parameters for an appropriate search ++ // 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)), +- userRoleId }); ++ doFilterEscaping(doAttributeValueEscaping(userRoleId)) }); + SearchControls controls = new SearchControls(); + if (roleSubtree) + controls.setSearchScope(SearchControls.SUBTREE_SCOPE); +-- +2.23.0 + diff --git a/CVE-2021-30640-6.patch b/CVE-2021-30640-6.patch new file mode 100644 index 0000000000000000000000000000000000000000..b56eb78f05203f6ca0fb9d54119030f1ca065b71 --- /dev/null +++ b/CVE-2021-30640-6.patch @@ -0,0 +1,32 @@ +From fd48ca875aaa46920b6d94fe737420d3985ad7d4 Mon Sep 17 00:00:00 2001 +From: Mark Thomas +Date: Tue, 13 Apr 2021 12:54:24 +0100 +Subject: [PATCH] Expanded tests to cover nested roles and fix escaping issues in search + +--- + java/org/apache/catalina/realm/JNDIRealm.java | 9 +++++++-- + 1 file changed, 7 insertions(+), 2 deletions(-) + +diff --git a/java/org/apache/catalina/realm/JNDIRealm.java b/java/org/apache/catalina/realm/JNDIRealm.java +index a3b6f86..cfe1c15 100644 +--- a/java/org/apache/catalina/realm/JNDIRealm.java ++++ b/java/org/apache/catalina/realm/JNDIRealm.java +@@ -2021,8 +2021,13 @@ System.out.println("userRoleName " + userRoleName + " " + attrs.get(userRoleName + Map newThisRound = new HashMap<>(); // Stores the groups we find in this iteration + + for (Entry group : newGroups.entrySet()) { +- filter = connection.roleFormat.format(new String[] { doFilterEscaping(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); +-- +2.23.0 + diff --git a/CVE-2021-30640-7.patch b/CVE-2021-30640-7.patch new file mode 100644 index 0000000000000000000000000000000000000000..66ad156eed48e736e9d724341c5309cd9671f5a7 --- /dev/null +++ b/CVE-2021-30640-7.patch @@ -0,0 +1,35 @@ +From 3383668c05becf01fe175aba928177b648f327ec Mon Sep 17 00:00:00 2001 +From: Mark Thomas +Date: Tue, 13 Apr 2021 14:47:07 +0100 +Subject: [PATCH] Expand testing to cover substitution in roleBase. Fix bugs. + +The code incorrectly referred to the original roleBase rather than the local version that includes the substituted value(s). +--- + java/org/apache/catalina/realm/JNDIRealm.java | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/java/org/apache/catalina/realm/JNDIRealm.java b/java/org/apache/catalina/realm/JNDIRealm.java +index cfe1c15..c78068b 100644 +--- a/java/org/apache/catalina/realm/JNDIRealm.java ++++ b/java/org/apache/catalina/realm/JNDIRealm.java +@@ -1988,7 +1988,7 @@ System.out.println("userRoleName " + userRoleName + " " + attrs.get(userRoleName + Attributes attrs = result.getAttributes(); + 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); +@@ -2033,7 +2033,7 @@ System.out.println("userRoleName " + userRoleName + " " + attrs.get(userRoleName + containerLog.trace("Perform a nested group search with base "+ roleBase + " and filter " + filter); + } + +- results = searchAsUser(connection.context, user, roleBase, filter, controls, ++ results = searchAsUser(connection.context, user, base, filter, controls, + isRoleSearchAsUser()); + + try { +-- +2.23.0 + diff --git a/CVE-2021-30640-8.patch b/CVE-2021-30640-8.patch new file mode 100644 index 0000000000000000000000000000000000000000..e4fb52ff6a5fe3ddb720ead7a1314a0bc7495efb --- /dev/null +++ b/CVE-2021-30640-8.patch @@ -0,0 +1,28 @@ +From c703ec491aca94cb17853808c7ce0c4fd99992bb Mon Sep 17 00:00:00 2001 +From: Mark Thomas +Date: Tue, 13 Apr 2021 15:19:31 +0100 +Subject: [PATCH] Expand tests to cover escaping of substituted roleBaes values + +While the UnboundedID LDAP SDK doesn't appear to have a preference some servers (Windows AD, OpenLDAP) do appear to. +--- + java/org/apache/catalina/realm/JNDIRealm.java | 4 +++- + 1 file changed, 3 insertions(+), 1 deletion(-) + +diff --git a/java/org/apache/catalina/realm/JNDIRealm.java b/java/org/apache/catalina/realm/JNDIRealm.java +index c78068b..7a8c5f6 100644 +--- a/java/org/apache/catalina/realm/JNDIRealm.java ++++ b/java/org/apache/catalina/realm/JNDIRealm.java +@@ -1967,7 +1967,9 @@ System.out.println("userRoleName " + userRoleName + " " + attrs.get(userRoleName + 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 { +-- +2.23.0 + diff --git a/CVE-2021-30640-pre1.patch b/CVE-2021-30640-pre1.patch new file mode 100644 index 0000000000000000000000000000000000000000..c0c2b1a02c64b4b41f1a9fed8678b6346878ad2a --- /dev/null +++ b/CVE-2021-30640-pre1.patch @@ -0,0 +1,45 @@ +From 700d26b69df3f1003ce8443d5569911c36b113de Mon Sep 17 00:00:00 2001 +From: Mark Thomas +Date: Tue, 5 Mar 2019 19:19:32 +0000 +Subject: [PATCH] Fix https://bz.apache.org/bugzilla/show_bug.cgi?id=63213 + +Ensure the correct escaping of group names when searching for nested +groups when the JNDIRealm is configured with roleNested set to true. +--- + java/org/apache/catalina/realm/JNDIRealm.java | 3 ++- + webapps/docs/changelog.xml | 5 +++++ + 2 files changed, 7 insertions(+), 1 deletion(-) + +diff --git a/java/org/apache/catalina/realm/JNDIRealm.java b/java/org/apache/catalina/realm/JNDIRealm.java +index e980bdf..034c0f0 100644 +--- a/java/org/apache/catalina/realm/JNDIRealm.java ++++ b/java/org/apache/catalina/realm/JNDIRealm.java +@@ -2010,7 +2010,8 @@ public class JNDIRealm extends RealmBase { + Map newThisRound = new HashMap<>(); // Stores the groups we find in this iteration + + for (Entry group : newGroups.entrySet()) { +- filter = roleFormat.format(new String[] { group.getKey(), group.getValue(), group.getValue() }); ++ filter = roleFormat.format(new String[] { doRFC2254Encoding(group.getKey()), ++ group.getValue(), group.getValue() }); + + if (containerLog.isTraceEnabled()) { + containerLog.trace("Perform a nested group search with base "+ roleBase + " and filter " + filter); +diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml +index 35b8eab..f088e0d 100644 +--- a/webapps/docs/changelog.xml ++++ b/webapps/docs/changelog.xml +@@ -55,6 +55,11 @@ + + Encode the output of the SSI printenv command. (markt) + ++ ++ 63213: Ensure the correct escaping of group names when ++ searching for nested groups when the JNDIRealm is configured with ++ roleNested set to true. (markt) ++ + + + +-- +2.23.0 + diff --git a/CVE-2021-30640-pre2.patch b/CVE-2021-30640-pre2.patch new file mode 100644 index 0000000000000000000000000000000000000000..2091756a412d7a7327e6fce510d95fbc72fc5843 --- /dev/null +++ b/CVE-2021-30640-pre2.patch @@ -0,0 +1,44 @@ +From 824c531393aa030f161e1ec352a65b7e9302d6b6 Mon Sep 17 00:00:00 2001 +From: Mark Thomas +Date: Fri, 26 Jul 2019 14:59:57 +0100 +Subject: [PATCH] Fix https://bz.apache.org/bugzilla/show_bug.cgi?id=63550 + +Only use the alternateURL for the JNDIRealm when it has been specified +--- + java/org/apache/catalina/realm/JNDIRealm.java | 4 ++++ + webapps/docs/changelog.xml | 4 ++++ + 2 files changed, 8 insertions(+) + +diff --git a/java/org/apache/catalina/realm/JNDIRealm.java b/java/org/apache/catalina/realm/JNDIRealm.java +index 034c0f0..505dd13 100644 +--- a/java/org/apache/catalina/realm/JNDIRealm.java ++++ b/java/org/apache/catalina/realm/JNDIRealm.java +@@ -2378,6 +2378,10 @@ public class JNDIRealm extends RealmBase { + context = createDirContext(getDirectoryContextEnvironment()); + + } catch (Exception e) { ++ if (alternateURL == null || alternateURL.length() == 0) { ++ // No alternate URL. Re-throw the exception. ++ throw e; ++ } + + connectionAttempt = 1; + +diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml +index f088e0d..7bcc3d9 100644 +--- a/webapps/docs/changelog.xml ++++ b/webapps/docs/changelog.xml +@@ -248,6 +248,10 @@ + + + ++ ++ 63550: Only try the alternateURL in the ++ JNDIRealm if one has been specified. (markt) ++ + + 50234: Add the capability to generate a web-fragment.xml file + to JspC. (markt) +-- +2.23.0 + diff --git a/CVE-2021-30640-pre3.patch b/CVE-2021-30640-pre3.patch new file mode 100644 index 0000000000000000000000000000000000000000..0cb317b34a808a972defd935ce812f302054d520 --- /dev/null +++ b/CVE-2021-30640-pre3.patch @@ -0,0 +1,1089 @@ +From 94b22be79a82a7238b022bcaa61b574e6a56691e Mon Sep 17 00:00:00 2001 +From: remm +Date: Thu, 30 Jan 2020 17:22:51 +0100 +Subject: [PATCH] Add connection pool to JNDI realm + +This implements a TODO from the class javadoc header. +As described in the javadoc, the idea is to use a pool to avoid blocking +on a single connection, which could possibly become a bottleneck in some +cases. The message formats need to be kept along with the connection +since they are not thread safe. +Preserve the default behavior: sync without pooling (using a Lock object +which is more flexible). +I may backport this since this is limited to the JNDI realm, but only +once it is confirmed to be regression free. Tested with ApacheDS but my +LDAP skills are very limited. +--- + java/org/apache/catalina/realm/JNDIRealm.java | 442 ++++++++++-------- + .../catalina/realm/LocalStrings.properties | 3 +- + .../apache/catalina/realm/TestJNDIRealm.java | 7 +- + webapps/docs/changelog.xml | 3 + + webapps/docs/config/realm.xml | 7 + + 5 files changed, 277 insertions(+), 185 deletions(-) + +diff --git a/java/org/apache/catalina/realm/JNDIRealm.java b/java/org/apache/catalina/realm/JNDIRealm.java +index 505dd13..b624c5b 100644 +--- a/java/org/apache/catalina/realm/JNDIRealm.java ++++ b/java/org/apache/catalina/realm/JNDIRealm.java +@@ -33,6 +33,8 @@ import java.util.List; + import java.util.Map; + import java.util.Map.Entry; + import java.util.Set; ++import java.util.concurrent.locks.Lock; ++import java.util.concurrent.locks.ReentrantLock; + + import javax.naming.AuthenticationException; + import javax.naming.CommunicationException; +@@ -62,6 +64,7 @@ import javax.net.ssl.SSLSession; + import javax.net.ssl.SSLSocketFactory; + + import org.apache.catalina.LifecycleException; ++import org.apache.tomcat.util.collections.SynchronizedStack; + import org.ietf.jgss.GSSCredential; + + /** +@@ -166,10 +169,6 @@ import org.ietf.jgss.GSSCredential; + * directory server itself. + * + * +- *

TODO - Support connection pooling (including message +- * format objects) so that authenticate() does not have to be +- * synchronized.

+- * + *

WARNING - There is a reported bug against the Netscape + * provider code (com.netscape.jndi.ldap.LdapContextFactory) with respect to + * successfully authenticated a non-existing user. The +@@ -208,12 +207,6 @@ public class JNDIRealm extends RealmBase { + protected String connectionURL = null; + + +- /** +- * The directory context linking us to our directory server. +- */ +- protected DirContext context = null; +- +- + /** + * The JNDI context factory used to acquire our InitialContext. By + * default, assumes use of an LDAP server using the standard JNDI LDAP +@@ -282,13 +275,6 @@ public class JNDIRealm extends RealmBase { + private boolean userSearchAsUser = false; + + +- /** +- * The MessageFormat object associated with the current +- * userSearch. +- */ +- protected MessageFormat userSearchFormat = null; +- +- + /** + * Should we search the entire subtree for matching users? + */ +@@ -328,32 +314,12 @@ public class JNDIRealm extends RealmBase { + protected String userPattern = null; + + +- /** +- * An array of MessageFormat objects associated with the current +- * userPatternArray. +- */ +- protected MessageFormat[] userPatternFormatArray = null; +- + /** + * The base element for role searches. + */ + protected String roleBase = ""; + + +- /** +- * The MessageFormat object associated with the current +- * roleBase. +- */ +- protected MessageFormat roleBaseFormat = null; +- +- +- /** +- * The MessageFormat object associated with the current +- * roleSearch. +- */ +- protected MessageFormat roleFormat = null; +- +- + /** + * The name of an attribute in the user's entry containing + * roles for that user +@@ -497,6 +463,30 @@ public class JNDIRealm extends RealmBase { + */ + private String sslProtocol; + ++ /** ++ * Non pooled connection to our directory server. ++ */ ++ protected JNDIConnection singleConnection = new JNDIConnection(); ++ ++ ++ /** ++ * The lock to ensure single connection thread safety. ++ */ ++ protected final Lock singleConnectionLock = new ReentrantLock(); ++ ++ ++ /** ++ * Connection pool. ++ */ ++ protected SynchronizedStack connectionPool = null; ++ ++ ++ /** ++ * The pool size limit. If 1, pooling is not used. ++ */ ++ protected int connectionPoolSize = 1; ++ ++ + // ------------------------------------------------------------- Properties + + /** +@@ -717,13 +707,8 @@ public class JNDIRealm extends RealmBase { + * @param userSearch The new user search pattern + */ + public void setUserSearch(String userSearch) { +- + this.userSearch = userSearch; +- if (userSearch == null) +- userSearchFormat = null; +- else +- userSearchFormat = new MessageFormat(userSearch); +- ++ singleConnection = create(); + } + + +@@ -796,13 +781,8 @@ public class JNDIRealm extends RealmBase { + * @param roleBase The new base element + */ + public void setRoleBase(String roleBase) { +- + this.roleBase = roleBase; +- if (roleBase == null) +- roleBaseFormat = null; +- else +- roleBaseFormat = new MessageFormat(roleBase); +- ++ singleConnection = create(); + } + + +@@ -844,13 +824,8 @@ public class JNDIRealm extends RealmBase { + * @param roleSearch The new role search pattern + */ + public void setRoleSearch(String roleSearch) { +- + this.roleSearch = roleSearch; +- if (roleSearch == null) +- roleFormat = null; +- else +- roleFormat = new MessageFormat(roleSearch); +- ++ singleConnection = create(); + } + + +@@ -960,18 +935,12 @@ public class JNDIRealm extends RealmBase { + * @param userPattern The new user pattern + */ + public void setUserPattern(String userPattern) { +- + this.userPattern = userPattern; +- if (userPattern == null) ++ if (userPattern == null) { + userPatternArray = null; +- else { ++ } else { + userPatternArray = parseUserPatternString(userPattern); +- int len = this.userPatternArray.length; +- userPatternFormatArray = new MessageFormat[len]; +- for (int i=0; i < len; i++) { +- userPatternFormatArray[i] = +- new MessageFormat(userPatternArray[i]); +- } ++ singleConnection = create(); + } + } + +@@ -1151,6 +1120,22 @@ public class JNDIRealm extends RealmBase { + this.cipherSuites = suites; + } + ++ /** ++ * @return the connection pool size, or the default value 1 if pooling ++ * is disabled ++ */ ++ public int getConnectionPoolSize() { ++ return connectionPoolSize; ++ } ++ ++ /** ++ * Set the connection pool size ++ * @param connectionPoolSize the new pool size ++ */ ++ public void setConnectionPoolSize(int connectionPoolSize) { ++ this.connectionPoolSize = connectionPoolSize; ++ } ++ + /** + * @return name of the {@link HostnameVerifier} class used for connections + * using StartTLS, or the empty string, if the default verifier +@@ -1269,20 +1254,20 @@ public class JNDIRealm extends RealmBase { + @Override + public Principal authenticate(String username, String credentials) { + +- DirContext context = null; ++ JNDIConnection connection = null; + Principal principal = null; + + try { + + // Ensure that we have a directory context available +- context = open(); ++ connection = get(); + + // Occasionally the directory context will timeout. Try one more + // time before giving up. + try { + + // Authenticate the specified username if possible +- principal = authenticate(context, username, credentials); ++ principal = authenticate(connection, username, credentials); + + } catch (NullPointerException | NamingException e) { + /* +@@ -1304,19 +1289,18 @@ public class JNDIRealm extends RealmBase { + containerLog.info(sm.getString("jndiRealm.exception.retry"), e); + + // close the connection so we know it will be reopened. +- if (context != null) +- close(context); ++ close(connection); + + // open a new directory context. +- context = open(); ++ connection = get(); + + // Try the authentication again. +- principal = authenticate(context, username, credentials); ++ principal = authenticate(connection, username, credentials); + } + + + // Release this context +- release(context); ++ release(connection); + + // Return the authenticated Principal (if any) + return principal; +@@ -1327,8 +1311,7 @@ public class JNDIRealm extends RealmBase { + containerLog.error(sm.getString("jndiRealm.exception"), e); + + // Close the connection so that it gets reopened next time +- if (context != null) +- close(context); ++ close(connection); + + // Return "not authenticated" for this request + if (containerLog.isDebugEnabled()) +@@ -1350,7 +1333,7 @@ public class JNDIRealm extends RealmBase { + * Return the Principal associated with the specified username and + * credentials, if there is one; otherwise return null. + * +- * @param context The directory context ++ * @param connection The directory context + * @param username Username of the Principal to look up + * @param credentials Password or other credentials to use in + * authenticating this username +@@ -1358,7 +1341,7 @@ public class JNDIRealm extends RealmBase { + * + * @exception NamingException if a directory server error occurs + */ +- public synchronized Principal authenticate(DirContext context, ++ public Principal authenticate(JNDIConnection connection, + String username, + String credentials) + throws NamingException { +@@ -1372,16 +1355,16 @@ public class JNDIRealm extends RealmBase { + + if (userPatternArray != null) { + for (int curUserPattern = 0; +- curUserPattern < userPatternFormatArray.length; ++ curUserPattern < userPatternArray.length; + curUserPattern++) { + // Retrieve user information +- User user = getUser(context, username, credentials, curUserPattern); ++ User user = getUser(connection, username, credentials, curUserPattern); + if (user != null) { + try { + // Check the user's credentials +- if (checkCredentials(context, user, credentials)) { ++ if (checkCredentials(connection.context, user, credentials)) { + // Search for additional roles +- List roles = getRoles(context, user); ++ List roles = getRoles(connection, user); + if (containerLog.isDebugEnabled()) { + containerLog.debug("Found roles: " + roles.toString()); + } +@@ -1400,16 +1383,16 @@ public class JNDIRealm extends RealmBase { + return null; + } else { + // Retrieve user information +- User user = getUser(context, username, credentials); ++ User user = getUser(connection, username, credentials); + if (user == null) + return null; + + // Check the user's credentials +- if (!checkCredentials(context, user, credentials)) ++ if (!checkCredentials(connection.context, user, credentials)) + return null; + + // Search for additional roles +- List roles = getRoles(context, user); ++ List roles = getRoles(connection, user); + if (containerLog.isDebugEnabled()) { + containerLog.debug("Found roles: " + roles.toString()); + } +@@ -1425,17 +1408,17 @@ public class JNDIRealm extends RealmBase { + * with the specified username, if found in the directory; + * otherwise return null. + * +- * @param context The directory context ++ * @param connection The directory context + * @param username Username to be looked up + * @return the User object + * @exception NamingException if a directory server error occurs + * +- * @see #getUser(DirContext, String, String, int) ++ * @see #getUser(JNDIConnection, String, String, int) + */ +- protected User getUser(DirContext context, String username) ++ protected User getUser(JNDIConnection connection, String username) + throws NamingException { + +- return getUser(context, username, null, -1); ++ return getUser(connection, username, null, -1); + } + + +@@ -1444,18 +1427,18 @@ public class JNDIRealm extends RealmBase { + * with the specified username, if found in the directory; + * otherwise return null. + * +- * @param context The directory context ++ * @param connection The directory context + * @param username Username to be looked up + * @param credentials User credentials (optional) + * @return the User object + * @exception NamingException if a directory server error occurs + * +- * @see #getUser(DirContext, String, String, int) ++ * @see #getUser(JNDIConnection, String, String, int) + */ +- protected User getUser(DirContext context, String username, String credentials) ++ protected User getUser(JNDIConnection connection, String username, String credentials) + throws NamingException { + +- return getUser(context, username, credentials, -1); ++ return getUser(connection, username, credentials, -1); + } + + +@@ -1470,14 +1453,14 @@ public class JNDIRealm extends RealmBase { + * configuration attribute is specified, all values of that + * attribute are retrieved from the directory entry. + * +- * @param context The directory context ++ * @param connection The directory context + * @param username Username to be looked up + * @param credentials User credentials (optional) + * @param curUserPattern Index into userPatternFormatArray + * @return the User object + * @exception NamingException if a directory server error occurs + */ +- protected User getUser(DirContext context, String username, ++ protected User getUser(JNDIConnection connection, String username, + String credentials, int curUserPattern) + throws NamingException { + +@@ -1496,8 +1479,8 @@ public class JNDIRealm extends RealmBase { + list.toArray(attrIds); + + // Use pattern or search for user entry +- if (userPatternFormatArray != null && curUserPattern >= 0) { +- user = getUserByPattern(context, username, credentials, attrIds, curUserPattern); ++ if (userPatternArray != null && curUserPattern >= 0) { ++ user = getUserByPattern(connection, username, credentials, attrIds, curUserPattern); + if (containerLog.isDebugEnabled()) { + containerLog.debug("Found user by pattern [" + user + "]"); + } +@@ -1505,12 +1488,12 @@ public class JNDIRealm extends RealmBase { + boolean thisUserSearchAsUser = isUserSearchAsUser(); + try { + if (thisUserSearchAsUser) { +- userCredentialsAdd(context, username, credentials); ++ userCredentialsAdd(connection.context, username, credentials); + } +- user = getUserBySearch(context, username, attrIds); ++ user = getUserBySearch(connection, username, attrIds); + } finally { + if (thisUserSearchAsUser) { +- userCredentialsRemove(context); ++ userCredentialsRemove(connection.context); + } + } + if (containerLog.isDebugEnabled()) { +@@ -1588,7 +1571,7 @@ public class JNDIRealm extends RealmBase { + * username and return a User object; otherwise return + * null. + * +- * @param context The directory context ++ * @param connection The directory context + * @param username The username + * @param credentials User credentials (optional) + * @param attrIds String[]containing names of attributes to +@@ -1597,7 +1580,7 @@ public class JNDIRealm extends RealmBase { + * @exception NamingException if a directory server error occurs + * @see #getUserByPattern(DirContext, String, String[], String) + */ +- protected User getUserByPattern(DirContext context, ++ protected User getUserByPattern(JNDIConnection connection, + String username, + String credentials, + String[] attrIds, +@@ -1606,25 +1589,25 @@ public class JNDIRealm extends RealmBase { + + User user = null; + +- if (username == null || userPatternFormatArray[curUserPattern] == null) ++ if (username == null || userPatternArray[curUserPattern] == null) + return null; + + // Form the dn from the user pattern +- String dn = userPatternFormatArray[curUserPattern].format(new String[] { username }); ++ String dn = connection.userPatternFormatArray[curUserPattern].format(new String[] { username }); + + try { +- user = getUserByPattern(context, username, attrIds, dn); ++ user = getUserByPattern(connection.context, username, attrIds, dn); + } catch (NameNotFoundException e) { + return null; + } catch (NamingException e) { + // If the getUserByPattern() call fails, try it again with the + // credentials of the user that we're searching for + try { +- userCredentialsAdd(context, dn, credentials); ++ userCredentialsAdd(connection.context, dn, credentials); + +- user = getUserByPattern(context, username, attrIds, dn); ++ user = getUserByPattern(connection.context, username, attrIds, dn); + } finally { +- userCredentialsRemove(context); ++ userCredentialsRemove(connection.context); + } + } + return user; +@@ -1636,22 +1619,22 @@ public class JNDIRealm extends RealmBase { + * information about the user with the specified username, if + * found in the directory; otherwise return null. + * +- * @param context The directory context ++ * @param connection The directory context + * @param username The username + * @param attrIds String[]containing names of attributes to retrieve. + * @return the User object + * @exception NamingException if a directory server error occurs + */ +- protected User getUserBySearch(DirContext context, ++ protected User getUserBySearch(JNDIConnection connection, + String username, + String[] attrIds) + throws NamingException { + +- if (username == null || userSearchFormat == null) ++ if (username == null || connection.userSearchFormat == null) + return null; + + // Form the search filter +- String filter = userSearchFormat.format(new String[] { username }); ++ String filter = connection.userSearchFormat.format(new String[] { username }); + + // Set up the search controls + SearchControls constraints = new SearchControls(); +@@ -1670,9 +1653,10 @@ public class JNDIRealm extends RealmBase { + if (attrIds == null) + attrIds = new String[0]; + constraints.setReturningAttributes(attrIds); ++System.out.println("getUserBySearch " + username); + + NamingEnumeration results = +- context.search(userBase, filter, constraints); ++ connection.context.search(userBase, filter, constraints); + + try { + // Fail if no entries found +@@ -1693,8 +1677,9 @@ public class JNDIRealm extends RealmBase { + // Check no further entries were found + try { + if (results.hasMore()) { +- if(containerLog.isInfoEnabled()) +- containerLog.info("username " + username + " has multiple entries"); ++ if (containerLog.isInfoEnabled()) { ++ containerLog.info(sm.getString("jndiRealm.multipleEntries", username)); ++ } + return null; + } + } catch (PartialResultException ex) { +@@ -1702,7 +1687,7 @@ public class JNDIRealm extends RealmBase { + throw ex; + } + +- String dn = getDistinguishedName(context, userBase, result); ++ String dn = getDistinguishedName(connection.context, userBase, result); + + if (containerLog.isTraceEnabled()) + containerLog.trace(" entry found for " + username + " with dn " + dn); +@@ -1724,6 +1709,7 @@ public class JNDIRealm extends RealmBase { + + // Retrieve values of userRoleName attribute + ArrayList roles = null; ++System.out.println("userRoleName " + userRoleName + " " + attrs.get(userRoleName)); + if (userRoleName != null) + roles = addAttributeValues(userRoleName, attrs, roles); + +@@ -1902,12 +1888,12 @@ public class JNDIRealm extends RealmBase { + * a directory search. If no roles are associated with this user, + * a zero-length List is returned. + * +- * @param context The directory context we are searching ++ * @param connection The directory context we are searching + * @param user The User to be checked + * @return the list of role names + * @exception NamingException if a directory server error occurs + */ +- protected List getRoles(DirContext context, User user) ++ protected List getRoles(JNDIConnection connection, User user) + throws NamingException { + + if (user == null) +@@ -1938,11 +1924,11 @@ public class JNDIRealm extends RealmBase { + } + + // Are we configured to do role searches? +- if ((roleFormat == null) || (roleName == null)) ++ if ((connection.roleFormat == null) || (roleName == null)) + return list; + + // Set up parameters for an appropriate search +- String filter = roleFormat.format(new String[] { doRFC2254Encoding(dn), username, userRoleId }); ++ String filter = connection.roleFormat.format(new String[] { doRFC2254Encoding(dn), username, userRoleId }); + SearchControls controls = new SearchControls(); + if (roleSubtree) + controls.setSearchScope(SearchControls.SUBTREE_SCOPE); +@@ -1951,20 +1937,20 @@ public class JNDIRealm extends RealmBase { + controls.setReturningAttributes(new String[] {roleName}); + + String base = null; +- if (roleBaseFormat != null) { +- NameParser np = context.getNameParser(""); ++ if (connection.roleBaseFormat != null) { ++ NameParser np = connection.context.getNameParser(""); + Name name = np.parse(dn); + String nameParts[] = new String[name.size()]; + for (int i = 0; i < name.size(); i++) { + nameParts[i] = name.get(i); + } +- base = roleBaseFormat.format(nameParts); ++ base = connection.roleBaseFormat.format(nameParts); + } else { + base = ""; + } + + // Perform the configured search and process the results +- NamingEnumeration results = searchAsUser(context, user, base, filter, controls, ++ NamingEnumeration results = searchAsUser(connection.context, user, base, filter, controls, + isRoleSearchAsUser()); + + if (results == null) +@@ -1977,7 +1963,7 @@ public class JNDIRealm extends RealmBase { + Attributes attrs = result.getAttributes(); + if (attrs == null) + continue; +- String dname = getDistinguishedName(context, roleBase, result); ++ String dname = getDistinguishedName(connection.context, roleBase, result); + String name = getAttributeValue(roleName, attrs); + if (name != null && dname != null) { + groupMap.put(dname, name); +@@ -2010,14 +1996,14 @@ public class JNDIRealm extends RealmBase { + Map newThisRound = new HashMap<>(); // Stores the groups we find in this iteration + + for (Entry group : newGroups.entrySet()) { +- filter = roleFormat.format(new String[] { doRFC2254Encoding(group.getKey()), ++ filter = connection.roleFormat.format(new String[] { doRFC2254Encoding(group.getKey()), + group.getValue(), group.getValue() }); + + if (containerLog.isTraceEnabled()) { + containerLog.trace("Perform a nested group search with base "+ roleBase + " and filter " + filter); + } + +- results = searchAsUser(context, user, roleBase, filter, controls, ++ results = searchAsUser(connection.context, user, roleBase, filter, controls, + isRoleSearchAsUser()); + + try { +@@ -2026,7 +2012,7 @@ public class JNDIRealm extends RealmBase { + Attributes attrs = result.getAttributes(); + if (attrs == null) + continue; +- String dname = getDistinguishedName(context, roleBase, result); ++ String dname = getDistinguishedName(connection.context, roleBase, result); + String name = getAttributeValue(roleName, attrs); + if (name != null && dname != null && !groupMap.keySet().contains(dname)) { + groupMap.put(dname, name); +@@ -2169,12 +2155,12 @@ public class JNDIRealm extends RealmBase { + /** + * Close any open connection to the directory server for this Realm. + * +- * @param context The directory context to be closed ++ * @param connection The directory context to be closed + */ +- protected void close(DirContext context) { ++ protected void close(JNDIConnection connection) { + + // Do nothing if there is no opened connection +- if (context == null) ++ if (connection.context == null) + return; + + // Close tls startResponse if used +@@ -2189,11 +2175,15 @@ public class JNDIRealm extends RealmBase { + try { + if (containerLog.isDebugEnabled()) + containerLog.debug("Closing directory context"); +- context.close(); ++ connection.context.close(); + } catch (NamingException e) { + containerLog.error(sm.getString("jndiRealm.close"), e); + } +- this.context = null; ++ connection.context = null; ++ // The lock will be reacquired before any manipulation of the connection ++ if (connectionPool == null) { ++ singleConnectionLock.unlock(); ++ } + + } + +@@ -2211,7 +2201,7 @@ public class JNDIRealm extends RealmBase { + } + + try { +- User user = getUser(open(), username, null); ++ User user = getUser(get(), username, null); + if (user == null) { + // User should be found... + return null; +@@ -2239,20 +2229,20 @@ public class JNDIRealm extends RealmBase { + protected Principal getPrincipal(String username, + GSSCredential gssCredential) { + +- DirContext context = null; ++ JNDIConnection connection = null; + Principal principal = null; + + try { + + // Ensure that we have a directory context available +- context = open(); ++ connection = get(); + + // Occasionally the directory context will timeout. Try one more + // time before giving up. + try { + + // Authenticate the specified username if possible +- principal = getPrincipal(context, username, gssCredential); ++ principal = getPrincipal(connection, username, gssCredential); + + } catch (CommunicationException | ServiceUnavailableException e) { + +@@ -2260,20 +2250,19 @@ public class JNDIRealm extends RealmBase { + containerLog.info(sm.getString("jndiRealm.exception.retry"), e); + + // close the connection so we know it will be reopened. +- if (context != null) +- close(context); ++ close(connection); + + // open a new directory context. +- context = open(); ++ connection = get(); + + // Try the authentication again. +- principal = getPrincipal(context, username, gssCredential); ++ principal = getPrincipal(connection, username, gssCredential); + + } + + + // Release this context +- release(context); ++ release(connection); + + // Return the authenticated Principal (if any) + return principal; +@@ -2284,8 +2273,7 @@ public class JNDIRealm extends RealmBase { + containerLog.error(sm.getString("jndiRealm.exception"), e); + + // Close the connection so that it gets reopened next time +- if (context != null) +- close(context); ++ close(connection); + + // Return "not authenticated" for this request + return null; +@@ -2298,19 +2286,20 @@ public class JNDIRealm extends RealmBase { + + /** + * Get the principal associated with the specified certificate. +- * @param context The directory context ++ * @param connection The directory context + * @param username The user name + * @param gssCredential The credentials + * @return the Principal associated with the given certificate. + * @exception NamingException if a directory server error occurs + */ +- protected synchronized Principal getPrincipal(DirContext context, ++ protected Principal getPrincipal(JNDIConnection connection, + String username, GSSCredential gssCredential) + throws NamingException { + + User user = null; + List roles = null; + Hashtable preservedEnvironment = null; ++ DirContext context = connection.context; + + try { + if (gssCredential != null && isUseDelegatedCredential()) { +@@ -2326,9 +2315,9 @@ public class JNDIRealm extends RealmBase { + // Note: Subject already set in SPNEGO authenticator so no need + // for Subject.doAs() here + } +- user = getUser(context, username); ++ user = getUser(connection, username); + if (user != null) { +- roles = getRoles(context, user); ++ roles = getRoles(connection, user); + } + } finally { + restoreEnvironmentParameter(context, +@@ -2363,50 +2352,100 @@ public class JNDIRealm extends RealmBase { + /** + * Open (if necessary) and return a connection to the configured + * directory server for this Realm. +- * @return the directory context ++ * @return the connection + * @exception NamingException if a directory server error occurs + */ +- protected DirContext open() throws NamingException { ++ protected JNDIConnection get() throws NamingException { ++ JNDIConnection connection = null; ++ // Use the pool if available, otherwise use the single connection ++ if (connectionPool != null) { ++ connection = connectionPool.pop(); ++ if (connection == null) { ++ connection = create(); ++ } ++ } else { ++ singleConnectionLock.lock(); ++ connection = singleConnection; ++ } ++ if (connection.context == null) { ++ open(connection); ++ } ++ return connection; ++ } + +- // Do nothing if there is a directory server connection already open +- if (context != null) +- return context; ++ /** ++ * Release our use of this connection so that it can be recycled. ++ * ++ * @param connection The directory context to release ++ */ ++ protected void release(JNDIConnection connection) { ++ if (connectionPool != null) { ++ if (!connectionPool.push(connection)) { ++ // Any connection that doesn't end back to the pool must be closed ++ close(connection); ++ } ++ } else { ++ singleConnectionLock.unlock(); ++ } ++ } + +- try { ++ /** ++ * Create a new connection wrapper, along with the ++ * message formats. ++ * @return the new connection ++ */ ++ protected JNDIConnection create() { ++ JNDIConnection connection = new JNDIConnection(); ++ if (userSearch != null) { ++ connection.userSearchFormat = new MessageFormat(userSearch); ++ } ++ if (userPattern != null) { ++ int len = userPatternArray.length; ++ connection.userPatternFormatArray = new MessageFormat[len]; ++ for (int i = 0; i < len; i++) { ++ connection.userPatternFormatArray[i] = ++ new MessageFormat(userPatternArray[i]); ++ } ++ } ++ if (roleBase != null) { ++ connection.roleBaseFormat = new MessageFormat(roleBase); ++ } ++ if (roleSearch != null) { ++ connection.roleFormat = new MessageFormat(roleSearch); ++ } ++ return connection; ++ } + ++ /** ++ * Create a new connection to the directory server. ++ * @param connection The directory server connection wrapper ++ * @throws NamingException if a directory server error occurs ++ */ ++ protected void open(JNDIConnection connection) throws NamingException { ++ try { + // Ensure that we have a directory context available +- context = createDirContext(getDirectoryContextEnvironment()); +- ++ connection.context = createDirContext(getDirectoryContextEnvironment()); + } catch (Exception e) { + if (alternateURL == null || alternateURL.length() == 0) { + // No alternate URL. Re-throw the exception. + throw e; + } +- + connectionAttempt = 1; +- + // log the first exception. + containerLog.info(sm.getString("jndiRealm.exception.retry"), e); +- + // Try connecting to the alternate url. +- context = createDirContext(getDirectoryContextEnvironment()); +- ++ connection.context = createDirContext(getDirectoryContextEnvironment()); + } finally { +- + // reset it in case the connection times out. + // the primary may come back. + connectionAttempt = 0; +- + } +- +- return context; +- + } + + @Override + public boolean isAvailable() { + // Simple best effort check +- return (context != null); ++ return (connectionPool != null || singleConnection.context != null); + } + + private DirContext createDirContext(Hashtable env) throws NamingException { +@@ -2559,18 +2598,6 @@ public class JNDIRealm extends RealmBase { + } + + +- /** +- * Release our use of this connection so that it can be recycled. +- * +- * @param context The directory context to release +- */ +- protected void release(DirContext context) { +- +- // NO-OP since we are not pooling anything +- +- } +- +- + // ------------------------------------------------------ Lifecycle Methods + + +@@ -2585,15 +2612,22 @@ public class JNDIRealm extends RealmBase { + @Override + protected void startInternal() throws LifecycleException { + ++ if (connectionPoolSize != 1) { ++ connectionPool = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE, connectionPoolSize); ++ } ++ + // Check to see if the connection to the directory can be opened ++ JNDIConnection connection = null; + try { +- open(); ++ connection = get(); + } catch (NamingException e) { + // A failure here is not fatal as the directory may be unavailable + // now but available later. Unavailability of the directory is not + // fatal once the Realm has started so there is no reason for it to + // be fatal when the Realm starts. + containerLog.error(sm.getString("jndiRealm.open"), e); ++ } finally { ++ release(connection); + } + + super.startInternal(); +@@ -2610,12 +2644,18 @@ public class JNDIRealm extends RealmBase { + */ + @Override + protected void stopInternal() throws LifecycleException { +- + super.stopInternal(); +- + // Close any open directory server connection +- close(this.context); +- ++ if (connectionPool == null) { ++ singleConnectionLock.lock(); ++ close(singleConnection); ++ } else { ++ JNDIConnection connection = null; ++ while ((connection = connectionPool.pop()) != null) { ++ close(connection); ++ } ++ connectionPool = null; ++ } + } + + /** +@@ -2812,5 +2852,43 @@ public class JNDIRealm extends RealmBase { + + + } ++ ++ /** ++ * Class holding the connection to the directory plus the associated ++ * non thread safe message formats. ++ */ ++ protected static class JNDIConnection { ++ ++ /** ++ * The MessageFormat object associated with the current ++ * userSearch. ++ */ ++ protected MessageFormat userSearchFormat = null; ++ ++ /** ++ * An array of MessageFormat objects associated with the current ++ * userPatternArray. ++ */ ++ protected MessageFormat[] userPatternFormatArray = null; ++ ++ /** ++ * The MessageFormat object associated with the current ++ * roleBase. ++ */ ++ protected MessageFormat roleBaseFormat = null; ++ ++ /** ++ * The MessageFormat object associated with the current ++ * roleSearch. ++ */ ++ protected MessageFormat roleFormat = null; ++ ++ /** ++ * The directory context linking us to our directory server. ++ */ ++ protected DirContext context = null; ++ ++ } ++ + } + +diff --git a/java/org/apache/catalina/realm/LocalStrings.properties b/java/org/apache/catalina/realm/LocalStrings.properties +index 1a96cc4..b66ad94 100644 +--- a/java/org/apache/catalina/realm/LocalStrings.properties ++++ b/java/org/apache/catalina/realm/LocalStrings.properties +@@ -48,6 +48,7 @@ jndiRealm.exception.retry=Exception performing authentication. Retrying... + jndiRealm.invalidHostnameVerifier=[{0}] not a valid class name for a HostnameVerifier + jndiRealm.invalidSslProtocol=Given protocol [{0}] is invalid. It has to be one of [{1}] + jndiRealm.invalidSslSocketFactory=[{0}] not a valid class name for a SSLSocketFactory ++jndiRealm.multipleEntries=User name [{0}] has multiple entries + jndiRealm.negotiatedTls=Negotiated tls connection using protocol [{0}] + jndiRealm.open=Exception opening directory server connection + jndiRealm.tlsClose=Exception closing tls response +@@ -93,4 +94,4 @@ lockOutRealm.removeWarning=User [{0}] was removed from the failed users cache af + credentialHandler.invalidStoredCredential=The invalid stored credential string [{0}] was provided by the Realm to match with the user provided credentials + credentialHandler.unableToMutateUserCredential=Failed to mutate user provided credentials. This typically means the CredentialHandler configuration is invalid + mdCredentialHandler.unknownEncoding=The encoding [{0}] is not supported so the current setting of [{1}] will still be used +-pbeCredentialHandler.invalidKeySpec=Unable to generate a password based key +\ No newline at end of file ++pbeCredentialHandler.invalidKeySpec=Unable to generate a password based key +diff --git a/test/org/apache/catalina/realm/TestJNDIRealm.java b/test/org/apache/catalina/realm/TestJNDIRealm.java +index b2a82e4..238106e 100644 +--- a/test/org/apache/catalina/realm/TestJNDIRealm.java ++++ b/test/org/apache/catalina/realm/TestJNDIRealm.java +@@ -117,9 +117,12 @@ public class TestJNDIRealm { + realm.setContainer(context); + realm.setUserSearch(""); + +- Field field = JNDIRealm.class.getDeclaredField("context"); ++ // Usually everything is created in create() but that's not the case here ++ Field field = JNDIRealm.class.getDeclaredField("singleConnection"); + field.setAccessible(true); +- field.set(realm, mockDirContext(mockSearchResults(password))); ++ Field field2 = JNDIRealm.JNDIConnection.class.getDeclaredField("context"); ++ field2.setAccessible(true); ++ field2.set(field.get(realm), mockDirContext(mockSearchResults(password))); + + realm.start(); + +diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml +index 7bcc3d9..173c209 100644 +--- a/webapps/docs/changelog.xml ++++ b/webapps/docs/changelog.xml +@@ -60,6 +60,9 @@ + searching for nested groups when the JNDIRealm is configured with + roleNested set to true. (markt) + ++ ++ Add connection pooling to JNDI realm. (remm) ++ + + + +diff --git a/webapps/docs/config/realm.xml b/webapps/docs/config/realm.xml +index 1d5ae6e..a4bc5ef 100644 +--- a/webapps/docs/config/realm.xml ++++ b/webapps/docs/config/realm.xml +@@ -433,6 +433,13 @@ + property.

+ + ++ ++

The JNDI realm can use a pool of connections to the directory server ++ to avoid blocking on a single connection. This attribute value is the ++ maximum pool size. If not specified, it will use 1, which ++ means a single connection will be used.

++
++ + +

The timeout in milliseconds to use when establishing the connection + to the LDAP directory. If not specified, a value of 5000 (5 seconds) is +-- +2.23.0 + diff --git a/CVE-2021-30640-pre4.patch b/CVE-2021-30640-pre4.patch new file mode 100644 index 0000000000000000000000000000000000000000..a055884792e5467a55367b300e16a7d0ae179824 --- /dev/null +++ b/CVE-2021-30640-pre4.patch @@ -0,0 +1,53 @@ +From 36710841d24807a6837757a24952ab5e6ced6ec8 Mon Sep 17 00:00:00 2001 +From: Mark Thomas +Date: Wed, 23 Jan 2019 15:09:37 +0000 +Subject: [PATCH] Refactor to simplify the fix for BZ 63026 + +git-svn-id: https://svn.apache.org/repos/asf/tomcat/tc8.5.x/trunk@1851939 13f79535-47bb-0310-9956-ffa450edef68 +--- + java/org/apache/catalina/realm/JNDIRealm.java | 8 ++++---- + 1 file changed, 4 insertions(+), 4 deletions(-) + +diff --git a/java/org/apache/catalina/realm/JNDIRealm.java b/java/org/apache/catalina/realm/JNDIRealm.java +index b624c5b..5714496 100644 +--- a/java/org/apache/catalina/realm/JNDIRealm.java ++++ b/java/org/apache/catalina/realm/JNDIRealm.java +@@ -2763,6 +2763,7 @@ System.out.println("userRoleName " + userRoleName + " " + attrs.get(userRoleName + // 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); +@@ -2774,9 +2775,8 @@ System.out.println("userRoleName " + userRoleName + " " + attrs.get(userRoleName + // Bugzilla 32269 + Name entryName = parser.parse(new CompositeName(resultName).get(0)); + +- Name name = contextName.addAll(baseName); ++ name = contextName.addAll(baseName); + name = name.addAll(entryName); +- return name.toString(); + } else { + if (containerLog.isTraceEnabled()) { + containerLog.trace(" search returned absolute name: " + resultName); +@@ -2792,14 +2792,14 @@ System.out.println("userRoleName " + userRoleName + " " + attrs.get(userRoleName + "Search returned unparseable absolute name: " + + resultName ); + } +- Name name = parser.parse(pathComponent.substring(1)); +- return name.toString(); ++ name = parser.parse(pathComponent.substring(1)); + } catch ( URISyntaxException e ) { + throw new InvalidNameException( + "Search returned unparseable absolute name: " + + resultName ); + } + } ++ return name.toString(); + } + + +-- +2.23.0 + diff --git a/CVE-2021-30640-pre5.patch b/CVE-2021-30640-pre5.patch new file mode 100644 index 0000000000000000000000000000000000000000..4226f75bc1274594c055feb2e4c27872731f2d0f --- /dev/null +++ b/CVE-2021-30640-pre5.patch @@ -0,0 +1,250 @@ +From 4bee1e769bce86cd53ce80eb18c15449ea0df34b Mon Sep 17 00:00:00 2001 +From: Mark Thomas +Date: Wed, 23 Jan 2019 15:11:07 +0000 +Subject: [PATCH] Add a new attribute, forceDnHexEscape, to the JNDIRealm that + forces escaping in the String representation of a distinguished name to use + the \nn form. This may avoid issues with realms using Active Directory which + appears to be more tolerant of optional escaping when the \nn form is used. + +git-svn-id: https://svn.apache.org/repos/asf/tomcat/tc8.5.x/trunk@1851941 13f79535-47bb-0310-9956-ffa450edef68 +--- + java/org/apache/catalina/realm/JNDIRealm.java | 95 ++++++++++++++++++- + .../TestJNDIRealmConvertToHexEscape.java | 70 ++++++++++++++ + webapps/docs/changelog.xml | 8 ++ + webapps/docs/config/realm.xml | 9 ++ + 4 files changed, 181 insertions(+), 1 deletion(-) + create mode 100644 test/org/apache/catalina/realm/TestJNDIRealmConvertToHexEscape.java + +diff --git a/java/org/apache/catalina/realm/JNDIRealm.java b/java/org/apache/catalina/realm/JNDIRealm.java +index 5714496..19fa704 100644 +--- a/java/org/apache/catalina/realm/JNDIRealm.java ++++ b/java/org/apache/catalina/realm/JNDIRealm.java +@@ -487,8 +487,19 @@ public class JNDIRealm extends RealmBase { + protected int connectionPoolSize = 1; + + ++ private boolean forceDnHexEscape = false; ++ ++ + // ------------------------------------------------------------- Properties + ++ public boolean getForceDnHexEscape() { ++ return forceDnHexEscape; ++ } ++ ++ public void setForceDnHexEscape(boolean forceDnHexEscape) { ++ this.forceDnHexEscape = forceDnHexEscape; ++ } ++ + /** + * @return the type of authentication to use. + */ +@@ -2799,7 +2810,89 @@ System.out.println("userRoleName " + userRoleName + " " + attrs.get(userRoleName + resultName ); + } + } +- return name.toString(); ++ ++ if (getForceDnHexEscape()) { ++ // Bug 63026 ++ return convertToHexEscape(name.toString()); ++ } else { ++ return name.toString(); ++ } ++ } ++ ++ ++ protected static String convertToHexEscape(String input) { ++ if (input.indexOf('\\') == -1) { ++ // No escaping present. Return original. ++ return input; ++ } ++ ++ // +6 allows for 3 escaped characters by default ++ StringBuilder result = new StringBuilder(input.length() + 6); ++ boolean previousSlash = false; ++ for (int i = 0; i < input.length(); i++) { ++ char c = input.charAt(i); ++ ++ if (previousSlash) { ++ switch (c) { ++ case ' ': { ++ result.append("\\20"); ++ break; ++ } ++ case '\"': { ++ result.append("\\22"); ++ break; ++ } ++ case '#': { ++ result.append("\\23"); ++ break; ++ } ++ case '+': { ++ result.append("\\2B"); ++ break; ++ } ++ case ',': { ++ result.append("\\2C"); ++ break; ++ } ++ case ';': { ++ result.append("\\3B"); ++ break; ++ } ++ case '<': { ++ result.append("\\3C"); ++ break; ++ } ++ case '=': { ++ result.append("\\3D"); ++ break; ++ } ++ case '>': { ++ result.append("\\3E"); ++ break; ++ } ++ case '\\': { ++ result.append("\\5C"); ++ break; ++ } ++ default: ++ result.append('\\'); ++ result.append(c); ++ } ++ previousSlash = false; ++ } else { ++ if (c == '\\') { ++ previousSlash = true; ++ } else { ++ result.append(c); ++ } ++ } ++ } ++ ++ if (previousSlash) { ++ result.append('\\'); ++ } ++ ++ return result.toString(); + } + + +diff --git a/test/org/apache/catalina/realm/TestJNDIRealmConvertToHexEscape.java b/test/org/apache/catalina/realm/TestJNDIRealmConvertToHexEscape.java +new file mode 100644 +index 0000000..8c610a3 +--- /dev/null ++++ b/test/org/apache/catalina/realm/TestJNDIRealmConvertToHexEscape.java +@@ -0,0 +1,70 @@ ++/* ++ * 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 TestJNDIRealmConvertToHexEscape { ++ ++ @Parameterized.Parameters(name = "{index}: in[{0}], out[{1}]") ++ public static Collection parameters() { ++ List parameterSets = new ArrayList<>(); ++ ++ parameterSets.add(new String[] { "none", "none" }); ++ parameterSets.add(new String[] { "\\", "\\" }); ++ parameterSets.add(new String[] { "\\\\", "\\5C" }); ++ parameterSets.add(new String[] { "\\5C", "\\5C" }); ++ parameterSets.add(new String[] { "\\ ", "\\20" }); ++ parameterSets.add(new String[] { "\\20", "\\20" }); ++ parameterSets.add(new String[] { "\\ foo", "\\20foo" }); ++ parameterSets.add(new String[] { "\\20foo", "\\20foo" }); ++ parameterSets.add(new String[] { "\\ foo", "\\20 foo" }); ++ parameterSets.add(new String[] { "\\20 foo", "\\20 foo" }); ++ parameterSets.add(new String[] { "\\ \\ foo", "\\20\\20foo" }); ++ parameterSets.add(new String[] { "\\20\\20foo", "\\20\\20foo" }); ++ parameterSets.add(new String[] { "foo\\ ", "foo\\20" }); ++ parameterSets.add(new String[] { "foo\\20", "foo\\20" }); ++ parameterSets.add(new String[] { "foo \\ ", "foo \\20" }); ++ parameterSets.add(new String[] { "foo \\20", "foo \\20" }); ++ parameterSets.add(new String[] { "foo\\ \\ ", "foo\\20\\20" }); ++ parameterSets.add(new String[] { "foo\\20\\20", "foo\\20\\20" }); ++ ++ return parameterSets; ++ } ++ ++ ++ @Parameter(0) ++ public String in; ++ @Parameter(1) ++ public String out; ++ ++ ++ @Test ++ public void testConvertToHexEscape() throws Exception { ++ String result = JNDIRealm.convertToHexEscape(in); ++ Assert.assertEquals(out, result); ++ } ++} +diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml +index 173c209..a7bb52c 100644 +--- a/webapps/docs/changelog.xml ++++ b/webapps/docs/changelog.xml +@@ -794,6 +794,14 @@ + a plain text response. Based on a suggestion from Muthukumar Marikani. + (markt) + ++ ++ 63026: Add a new attribute, forceDnHexEscape, to ++ the JNDIRealm that forces escaping in the String ++ representation of a distinguished name to use the \nn form. ++ This may avoid issues with realms using Active Directory which appears ++ to be more tolerant of optional escaping when the \nn form ++ is used. (markt) ++ + + + +diff --git a/webapps/docs/config/realm.xml b/webapps/docs/config/realm.xml +index a4bc5ef..715ceb7 100644 +--- a/webapps/docs/config/realm.xml ++++ b/webapps/docs/config/realm.xml +@@ -463,6 +463,15 @@ + "finding" and "searching". If not specified, "always" is used.

+
+ ++ ++

A setting of true forces escaping in the String ++ representation of a distinguished name to use the \nn form. ++ This may avoid issues with realms using Active Directory which appears ++ to be more tolerant of optional escaping when the \nn form ++ is used. If not specified, the default of false will be ++ used.

++
++ + +

The name of the class to use for hostname verification when + using StartTLS for securing the connection to the ldap server. +-- +2.23.0 + diff --git a/tomcat.spec b/tomcat.spec index e85fb6664baab99d77fb517bb1d87ea7a79ff5df..aa7acf646ffca4bfed651f640f3bf577d995c413 100644 --- a/tomcat.spec +++ b/tomcat.spec @@ -13,7 +13,7 @@ Name: tomcat Epoch: 1 Version: %{major_version}.%{minor_version}.%{micro_version} -Release: 19 +Release: 20 Summary: Implementation of the Java Servlet, JavaServer Pages, Java Expression Language and Java WebSocket technologies License: ASL 2.0 URL: http://tomcat.apache.org/ @@ -85,6 +85,19 @@ Patch6040: CVE-2021-25329.patch Patch6041: CVE-2021-33037-1.patch Patch6042: CVE-2021-33037-2.patch Patch6043: CVE-2021-33037-3.patch +Patch6044: CVE-2021-30640-pre1.patch +Patch6045: CVE-2021-30640-pre2.patch +Patch6046: CVE-2021-30640-pre3.patch +Patch6047: CVE-2021-30640-pre4.patch +Patch6048: CVE-2021-30640-pre5.patch +Patch6049: CVE-2021-30640-1.patch +Patch6050: CVE-2021-30640-2.patch +Patch6051: CVE-2021-30640-3.patch +Patch6052: CVE-2021-30640-4.patch +Patch6053: CVE-2021-30640-5.patch +Patch6054: CVE-2021-30640-6.patch +Patch6055: CVE-2021-30640-7.patch +Patch6056: CVE-2021-30640-8.patch BuildRequires: ecj >= 1:4.6.1 findutils apache-commons-collections apache-commons-daemon BuildRequires: apache-commons-dbcp apache-commons-pool tomcat-taglibs-standard ant @@ -486,6 +499,9 @@ fi %{_javadocdir}/%{name} %changelog +* Thu Jul 29 2021 wangyue - 1:9.0.10-20 +- Fix CVE-2021-30640 + * Mon Jul 19 2021 wangyue - 1:9.0.10-19 - Fix CVE-2021-33037