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