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]
