This is an automated email from the ASF dual-hosted git repository.

rmaucher pushed a commit to branch 11.0.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 54965e78d8be93d8f1a6f1379d7fbe842d05a8b2
Author: opencode <[email protected]>
AuthorDate: Thu Nov 5 20:07:12 2026 +0100

    Mask nested interceptor credential properties
    
    PoolProperties.maskInterceptorCredentials only consumed the first 
parenthesised list of each interceptor definition. A credential typed inside a 
nested list, such as StatementCache(foo=(password=secret)), was copied verbatim 
into configuration dumps and the JMX and bean views.
    
    Track parenthesis depth while reading each definition, treat a comma at a 
property-list depth as a pair separator, and mask a recognised credential 
wherever it appears. Add a regression test covering nested lists.
---
 .../apache/tomcat/jdbc/pool/PoolProperties.java    | 125 ++++++++++++++-------
 .../tomcat/jdbc/pool/TestPoolProperties.java       |  17 +++
 2 files changed, 104 insertions(+), 38 deletions(-)

diff --git 
a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PoolProperties.java
 
b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PoolProperties.java
index 62dca40e7e..0775899afa 100644
--- 
a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PoolProperties.java
+++ 
b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/PoolProperties.java
@@ -1269,44 +1269,7 @@ public class PoolProperties implements 
PoolConfiguration, Cloneable, Serializabl
             if (segmentEnd < 0) {
                 segmentEnd = value.length();
             }
-            int cursor = segmentStart;
-            int open = value.indexOf('(', cursor);
-            while (open >= 0 && open < segmentEnd) {
-                result.append(value, cursor, open + 1);
-                int close = value.indexOf(')', open + 1);
-                if (close < 0 || close > segmentEnd) {
-                    // Never closed inside this segment (the closing
-                    // parenthesis of a later definition does not close this
-                    // one either): mask whatever was typed as a property to
-                    // the end of the segment.
-                    close = segmentEnd;
-                }
-                int pairStart = open + 1;
-                while (pairStart < close) {
-                    int pairEnd = value.indexOf(',', pairStart);
-                    if (pairEnd < 0 || pairEnd >= close) {
-                        pairEnd = close;
-                    }
-                    int separator = value.indexOf('=', pairStart);
-                    if (separator >= 0 && separator < pairEnd
-                            && isCredentialKey(value.substring(pairStart, 
separator).trim())) {
-                        result.append(value, pairStart, separator + 1);
-                        result.append(PASSWORD_MASK);
-                    } else {
-                        result.append(value, pairStart, pairEnd);
-                    }
-                    if (pairEnd < close) {
-                        result.append(',');
-                    }
-                    pairStart = pairEnd + 1;
-                }
-                cursor = close;
-                open = value.indexOf('(', cursor);
-            }
-            // What is left of the segment - the class name before the first
-            // parenthesis and the text after the last closed property list -
-            // is outside every property list.
-            result.append(value, cursor, segmentEnd);
+            maskInterceptorSegment(value, segmentStart, segmentEnd, result);
             if (segmentEnd < value.length()) {
                 // Keep the definition separator itself.
                 result.append(';');
@@ -1317,6 +1280,92 @@ public class PoolProperties implements 
PoolConfiguration, Cloneable, Serializabl
     }
 
 
+    /**
+     * Appends one definition segment, masking a recognised credential pair at
+     * any parenthesis depth. A {@code (} or {@code )} changes the depth and a
+     * comma at a property-list depth starts a new pair; a pair typed inside a
+     * nested list is therefore read the same way as one in the list the
+     * interceptor parser consumes. A list that never closes is read to the end
+     * of the segment.
+     *
+     * @param value the string being masked
+     * @param start the start of the segment
+     * @param end the end of the segment, exclusive
+     * @param result the masked output
+     */
+    private static void maskInterceptorSegment(String value, int start, int 
end, StringBuilder result) {
+        int depth = 0;
+        int i = start;
+        boolean pairStart = false;
+        while (i < end) {
+            char c = value.charAt(i);
+            if (c == '(') {
+                result.append(c);
+                depth++;
+                i++;
+                pairStart = depth > 0;
+                continue;
+            }
+            if (c == ')') {
+                result.append(c);
+                if (depth > 0) {
+                    depth--;
+                }
+                i++;
+                pairStart = false;
+                continue;
+            }
+            if (c == ',' && depth > 0) {
+                result.append(c);
+                i++;
+                pairStart = true;
+                continue;
+            }
+            if (pairStart) {
+                int separator = -1;
+                int j = i;
+                while (j < end) {
+                    char next = value.charAt(j);
+                    if (next == '=') {
+                        separator = j;
+                        break;
+                    }
+                    if (next == ',' || next == ')') {
+                        break;
+                    }
+                    j++;
+                }
+                if (separator >= 0 && isCredentialKey(value.substring(i, 
separator).trim())) {
+                    result.append(value, i, separator + 1);
+                    result.append(PASSWORD_MASK);
+                    int valueEnd = separator + 1;
+                    int valueDepth = 0;
+                    while (valueEnd < end) {
+                        char next = value.charAt(valueEnd);
+                        if (next == '(') {
+                            valueDepth++;
+                        } else if (next == ')') {
+                            if (valueDepth == 0) {
+                                break;
+                            }
+                            valueDepth--;
+                        } else if (next == ',' && valueDepth == 0) {
+                            break;
+                        }
+                        valueEnd++;
+                    }
+                    i = valueEnd;
+                    pairStart = false;
+                    continue;
+                }
+                pairStart = false;
+            }
+            result.append(c);
+            i++;
+        }
+    }
+
+
     /**
      * Masks the credential values carried by an array of parsed interceptor
      * definitions, the display counterpart of
diff --git 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestPoolProperties.java
 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestPoolProperties.java
index b3faf2c14f..44fc5ac93d 100644
--- 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestPoolProperties.java
+++ 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestPoolProperties.java
@@ -835,6 +835,23 @@ class TestPoolProperties {
                 "the class name and the other properties must stay readable, 
was: " + asString);
     }
 
+    @Test
+    void toStringMasksAnInterceptorCredentialInANestedPropertyList() {
+        PoolProperties properties = new PoolProperties();
+        properties.setJdbcInterceptors(
+                
"StatementCache(foo=(password=interceptor-secret),pwd=Shouted-Secret)");
+
+        String asString = properties.toString();
+
+        Assertions.assertFalse(asString.contains("interceptor-secret"),
+                "a credential nested inside another property list must not 
leak, was: " + asString);
+        Assertions.assertFalse(asString.contains("Shouted-Secret"),
+                "the pair after the nested list must still be masked, was: " + 
asString);
+        Assertions.assertTrue(asString.contains(
+                        
"StatementCache(foo=(password=********),pwd=********)"),
+                "the surrounding structure must stay readable, was: " + 
asString);
+    }
+
     @Test
     void 
toStringMasksAnInterceptorCredentialWrittenAroundSpacesAndLeavesOtherPairsAlone()
 {
         PoolProperties properties = new PoolProperties();


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to