From 6a719704236d3ce02100606290ff59b6a11f6b20 Mon Sep 17 00:00:00 2001
From: Mark Thomas <markt@apache.org>
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<Object[]> parameters() {
+        List<Object[]> 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", "\\3Ctest" });
+        parameterSets.add(new String[] { "te<st", "te\\3Cst" });
+        parameterSets.add(new String[] { "test<", "test\\3C" });
+        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

