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

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

commit 358ffc6051d0f8cdfe1ae51cb3b4a2d6af545e6b
Author: opencode <[email protected]>
AuthorDate: Thu Oct 29 23:56:58 2026 +0100

    Mask credential values embedded in the url and connectionProperties 
diagnostics output
    
    The masking added for the dedicated password property in
    PoolProperties.toString() covered only the field itself, while the url
    and connectionProperties raw strings were printed verbatim, so a JDBC
    URL such as jdbc:mysql://host/db?password=xxx or a connectionProperties
    pair like password=inline-secret appeared in cleartext wherever the
    dump was surfaced: JMX reads, diagnostics logging and
    DataSource.toString(). The two raw strings now pass through a linear
    scan that replaces the value of a case-insensitive password, pass or
    pwd key at a segment boundary with the same mask the field uses,
    matching the key segmentation of the real properties parser; lookalike
    keys such as compass or passwordfile are left readable. Forms that
    would need full URL-dialect parsing, such as the userinfo component,
    remain documented as not recognised. This is defence-in-depth for
    diagnostics output of trusted configuration, not a security fix.
---
 .../apache/tomcat/jdbc/pool/PoolProperties.java    | 107 ++++++++++++++++++++-
 .../tomcat/jdbc/pool/TestPoolProperties.java       |  44 +++++++++
 2 files changed, 149 insertions(+), 2 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 1782e75782..d3c744574e 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
@@ -41,6 +41,9 @@ public class PoolProperties implements PoolConfiguration, 
Cloneable, Serializabl
     private static final long serialVersionUID = -8519283440854213745L;
     private static final Log log = LogFactory.getLog(PoolProperties.class);
 
+    /** The mask the {@link #toString()} dump prints in place of a password. */
+    private static final String PASSWORD_MASK = "********";
+
     /**
      * Constructs a PoolProperties with default values.
      */
@@ -781,12 +784,19 @@ public class PoolProperties implements PoolConfiguration, 
Cloneable, Serializabl
                     } catch (NoSuchMethodException nm) {
                         continue;
                     }
+                    Object value = m.invoke(this, new Object[0]);
                     buf.append(field);
                     buf.append('=');
                     if (DataSourceFactory.PROP_PASSWORD.equals(field)) {
-                        buf.append("********");
+                        buf.append(PASSWORD_MASK);
+                    } else if (DataSourceFactory.PROP_URL.equals(field)
+                            || 
DataSourceFactory.PROP_CONNECTIONPROPERTIES.equals(field)) {
+                        // The same dump prints these raw configuration
+                        // strings, so a credential typed into one of them
+                        // has to be masked too, not just the password field.
+                        buf.append(value == null ? "null" : 
maskCredentials((String) value));
                     } else {
-                        buf.append(m.invoke(this, new Object[0]));
+                        buf.append(value);
                     }
                     buf.append("; ");
                     break;
@@ -802,6 +812,99 @@ public class PoolProperties implements PoolConfiguration, 
Cloneable, Serializabl
         return buf.toString();
     }
 
+
+    /**
+     * Masks the value of credential-like {@code key=value} pairs embedded in a
+     * raw configuration string so that a password typed into the JDBC URL 
query
+     * string or the connection properties is not printed in cleartext the way
+     * the dedicated password field is masked in {@link #toString()}. The keys
+     * recognised, compared ignoring case, are {@code password}, {@code pass}
+     * and {@code pwd}. A key is honoured only at a segment boundary - the 
start
+     * of the string or one of {@code ;}, {@code &} or {@code ?} - so a key 
such
+     * as {@code compass} is left alone, and its value runs to the next
+     * {@code ;}, {@code &} or the end of the string.
+     * <p>
+     * Only the {@code key=value} shape is scrubbed. The
+     * {@code user:password@host} userinfo form, the {@code key:value} and
+     * {@code key value} forms that a {@link java.util.Properties} reader
+     * additionally accepts, and driver-specific secrets that use a key other
+     * than the three above are not recognised; recognising them would require
+     * a full parse of each driver's URL dialect.
+     *
+     * @param value the raw configuration string, may be {@code null}
+     *
+     * @return the string with every credential key's value replaced by the 
mask,
+     *         or the string unchanged when it holds no recognised credential 
key
+     */
+    private static String maskCredentials(String value) {
+        if (value == null || value.indexOf('=') < 0) {
+            return value;
+        }
+        StringBuilder result = new StringBuilder(value.length());
+        int i = 0;
+        while (i < value.length()) {
+            int equals = value.indexOf('=', i);
+            if (equals < 0) {
+                break;
+            }
+            int keyStart = equals;
+            while (keyStart > 0 && 
!isCredentialSegmentSeparator(value.charAt(keyStart - 1))) {
+                keyStart--;
+            }
+            String key = value.substring(keyStart, equals).trim();
+            int valueStart = equals + 1;
+            int valueEnd = value.length();
+            for (int k = valueStart; k < value.length(); k++) {
+                char c = value.charAt(k);
+                if (c == ';' || c == '&') {
+                    valueEnd = k;
+                    break;
+                }
+            }
+            result.append(value, i, equals);
+            result.append('=');
+            if (isCredentialKey(key)) {
+                result.append(PASSWORD_MASK);
+            } else {
+                result.append(value, valueStart, valueEnd);
+            }
+            // Resume after the value so a '=' inside a masked value cannot be
+            // mistaken for the start of a new key.
+            i = valueEnd;
+        }
+        result.append(value, i, value.length());
+        return result.toString();
+    }
+
+
+    /**
+     * Whether {@code c} ends one configuration segment and starts the next,
+     * covering the URL query separators and the semicolon used by the
+     * connection properties string.
+     *
+     * @param c the character to test
+     *
+     * @return {@code true} when the character is a segment boundary
+     */
+    private static boolean isCredentialSegmentSeparator(char c) {
+        return c == ';' || c == '&' || c == '?';
+    }
+
+
+    /**
+     * Whether {@code key} names a credential, ignoring case and surrounding
+     * whitespace.
+     *
+     * @param key the extracted key
+     *
+     * @return {@code true} when the key is one of the recognised credential 
keys
+     */
+    private static boolean isCredentialKey(String key) {
+        return DataSourceFactory.PROP_PASSWORD.equalsIgnoreCase(key)
+                || "pass".equalsIgnoreCase(key)
+                || "pwd".equalsIgnoreCase(key);
+    }
+
     /**
      * Returns the current pool counter value.
      *
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 bfd2202d6d..689a8a73ed 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
@@ -249,4 +249,48 @@ class TestPoolProperties {
         Assertions.assertTrue(asString.contains("jdbc:h2:mem:shown"),
                 "the JDBC URL must stay readable, was: " + asString);
     }
+
+    @Test
+    void toStringMasksAPasswordEmbeddedInTheConnectionProperties() {
+        PoolProperties properties = new PoolProperties();
+        
properties.setConnectionProperties("socketTimeout=1000;password=inline-secret;PWD=Shouted-Secret");
+
+        String asString = properties.toString();
+
+        Assertions.assertFalse(asString.contains("inline-secret"),
+                "the password embedded in the string must not leak, was: " + 
asString);
+        Assertions.assertFalse(asString.contains("Shouted-Secret"),
+                "the key must be recognised ignoring case, was: " + asString);
+        
Assertions.assertTrue(asString.contains("connectionProperties=socketTimeout=1000;password=********;PWD=********"),
+                "the other pair must keep printing with the pair masked, was: 
" + asString);
+    }
+
+    @Test
+    void toStringMasksAPasswordParameterInTheUrl() {
+        PoolProperties properties = new PoolProperties();
+        
properties.setUrl("jdbc:mysql://localhost:3306/db?user=me&password=url-secret&sessionVariables=foo=bar&ssl=true");
+
+        String asString = properties.toString();
+
+        Assertions.assertFalse(asString.contains("url-secret"),
+                "the password in the URL query must not leak, was: " + 
asString);
+        
Assertions.assertTrue(asString.contains("url=jdbc:mysql://localhost:3306/db?user=me"
+                + "&password=********&sessionVariables=foo=bar&ssl=true"),
+                "the rest of the URL must stay readable, was: " + asString);
+    }
+
+    @Test
+    void toStringKeepsNonCredentialPairsInsideRawStringsUntouched() {
+        PoolProperties properties = new PoolProperties();
+        properties.setUrl("jdbc:h2:mem:shown");
+        
properties.setConnectionProperties("socketTimeout=1000;mypassword=not-the-key;passwordfile=/etc/pw");
+
+        String asString = properties.toString();
+
+        
Assertions.assertTrue(asString.contains("connectionProperties=socketTimeout=1000;"
+                        + "mypassword=not-the-key;passwordfile=/etc/pw"),
+                "only the whole keys password, pass and pwd may be masked, 
was: " + asString);
+        Assertions.assertTrue(asString.contains("url=jdbc:h2:mem:shown"),
+                "a URL without an equals sign must come out unchanged, was: " 
+ asString);
+    }
 }


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

Reply via email to