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

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


The following commit(s) were added to refs/heads/main by this push:
     new b5bae007955e CAMEL-24256: Mask URI userinfo credentials and PEM 
private keys
b5bae007955e is described below

commit b5bae007955ed6bf64e608affd4d383253e8b8d5
Author: Omar Atie <[email protected]>
AuthorDate: Mon Jul 27 00:36:34 2026 -0700

    CAMEL-24256: Mask URI userinfo credentials and PEM private keys
    
    Extend SensitiveUtils and DefaultMaskingFormatter with value-shape masking
    for connection-string userinfo passwords and PEM private-key blocks so
    logMask and other masker callers cover secrets that have no sensitive key
    name. Includes fast-path guards before regex, Matcher.quoteReplacement
    consistency fix for keyword-based masking, and upgrade guide entry.
    
    Closes #25113
    
    Co-authored-by: Cursor <[email protected]>
---
 .../org/apache/camel/catalog/docs/log-eip.adoc     |   4 +
 .../src/main/docs/modules/eips/pages/log-eip.adoc  |   4 +
 .../processor/DefaultMaskingFormatterTest.java     | 202 ++++++++++++++++-----
 .../support/processor/DefaultMaskingFormatter.java |  42 +++--
 .../java/org/apache/camel/util/SensitiveUtils.java |  82 +++++++++
 .../org/apache/camel/util/SensitiveUtilsTest.java  | 194 +++++++++++++++++---
 .../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc    |  18 ++
 7 files changed, 458 insertions(+), 88 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/log-eip.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/log-eip.adoc
index f6b636d3fd82..4e11a93b03c6 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/log-eip.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/log-eip.adoc
@@ -502,6 +502,10 @@ The know set of keywords to mask is gathered from all the 
different component op
 The list is generated into the source code in 
`org.apache.camel.util.SensitiveUtils`.
 At this time of writing, there are more than 65 different keywords.
 
+In addition to name-based masking, `DefaultMaskingFormatter` also masks two 
value shapes that have no
+sensitive key name: URI userinfo passwords (`scheme://user:password@host`) and 
PEM private-key blocks
+(`-----BEGIN … PRIVATE KEY-----`). Certificates and public keys are left 
unchanged.
+
 Custom keywords can be added as shown:
 
 ._Java-only: registering a custom masking formatter with additional keywords_
diff --git 
a/core/camel-core-engine/src/main/docs/modules/eips/pages/log-eip.adoc 
b/core/camel-core-engine/src/main/docs/modules/eips/pages/log-eip.adoc
index f6b636d3fd82..4e11a93b03c6 100644
--- a/core/camel-core-engine/src/main/docs/modules/eips/pages/log-eip.adoc
+++ b/core/camel-core-engine/src/main/docs/modules/eips/pages/log-eip.adoc
@@ -502,6 +502,10 @@ The know set of keywords to mask is gathered from all the 
different component op
 The list is generated into the source code in 
`org.apache.camel.util.SensitiveUtils`.
 At this time of writing, there are more than 65 different keywords.
 
+In addition to name-based masking, `DefaultMaskingFormatter` also masks two 
value shapes that have no
+sensitive key name: URI userinfo passwords (`scheme://user:password@host`) and 
PEM private-key blocks
+(`-----BEGIN … PRIVATE KEY-----`). Certificates and public keys are left 
unchanged.
+
 Custom keywords can be added as shown:
 
 ._Java-only: registering a custom masking formatter with additional keywords_
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/support/processor/DefaultMaskingFormatterTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/support/processor/DefaultMaskingFormatterTest.java
index 73466b164633..1e38b600c8e9 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/support/processor/DefaultMaskingFormatterTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/support/processor/DefaultMaskingFormatterTest.java
@@ -16,137 +16,245 @@
  */
 package org.apache.camel.support.processor;
 
+import java.util.Collections;
+
 import org.junit.jupiter.api.Test;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.assertj.core.api.Assertions.assertThat;
 
-public class DefaultMaskingFormatterTest {
+class DefaultMaskingFormatterTest {
 
     @Test
-    public void testDefaultOption() {
+    void testDefaultOption() {
         DefaultMaskingFormatter formatter = new DefaultMaskingFormatter();
         String answer
                 = formatter.format("key=value, myPassword=foo,\n 
myPassphrase=\"foo bar\", secretKey='!@#$%^&*() -+[]{};:'");
-        assertEquals("key=value, myPassword=xxxxx,\n myPassphrase=\"xxxxx\", 
secretKey='xxxxx'", answer);
+        assertThat(answer).isEqualTo("key=value, myPassword=xxxxx,\n 
myPassphrase=\"xxxxx\", secretKey='xxxxx'");
 
         answer = formatter.format("<xmlPassword>\n foo bar 
\n</xmlPassword>\n<user password=\"asdf qwert\"/>");
-        assertEquals("<xmlPassword>\n xxxxx \n</xmlPassword>\n<user 
password=\"xxxxx\"/>", answer);
+        assertThat(answer).isEqualTo("<xmlPassword>\n xxxxx 
\n</xmlPassword>\n<user password=\"xxxxx\"/>");
 
         answer = formatter.format(
                 "{\"key\" : \"value\", \"Password\":\"foo\", \"Passphrase\" : 
\"foo bar\", \"SecretKey\" : \"!@#$%^&*() -+[]{};:'\"}");
-        assertEquals(
-                "{\"key\" : \"value\", \"Password\":\"xxxxx\", \"Passphrase\" 
: \"xxxxx\", \"SecretKey\" : \"xxxxx\"}",
-                answer);
+        assertThat(answer).isEqualTo(
+                "{\"key\" : \"value\", \"Password\":\"xxxxx\", \"Passphrase\" 
: \"xxxxx\", \"SecretKey\" : \"xxxxx\"}");
     }
 
     @Test
-    public void testDisableKeyValueMask() {
+    void testDisableKeyValueMask() {
         DefaultMaskingFormatter formatter = new DefaultMaskingFormatter(false, 
true, true);
         String answer
                 = formatter.format("key=value, myPassword=foo,\n 
myPassphrase=\"foo bar\", secretKey='!@#$%^&*() -+[]{};:'");
-        assertEquals("key=value, myPassword=foo,\n myPassphrase=\"foo bar\", 
secretKey='!@#$%^&*() -+[]{};:'", answer);
+        assertThat(answer).isEqualTo("key=value, myPassword=foo,\n 
myPassphrase=\"foo bar\", secretKey='!@#$%^&*() -+[]{};:'");
 
         answer = formatter.format("<xmlPassword>\n foo bar 
\n</xmlPassword>\n<user password=\"asdf qwert\"/>");
-        assertEquals("<xmlPassword>\n xxxxx \n</xmlPassword>\n<user 
password=\"asdf qwert\"/>", answer);
+        assertThat(answer).isEqualTo("<xmlPassword>\n xxxxx 
\n</xmlPassword>\n<user password=\"asdf qwert\"/>");
 
         answer = formatter.format(
                 "{\"key\" : \"value\", \"Password\":\"foo\", \"Passphrase\" : 
\"foo bar\", \"SecretKey\" : \"!@#$%^&*() -+[]{};:'\"}");
-        assertEquals(
-                "{\"key\" : \"value\", \"Password\":\"xxxxx\", \"Passphrase\" 
: \"xxxxx\", \"SecretKey\" : \"xxxxx\"}",
-                answer);
+        assertThat(answer).isEqualTo(
+                "{\"key\" : \"value\", \"Password\":\"xxxxx\", \"Passphrase\" 
: \"xxxxx\", \"SecretKey\" : \"xxxxx\"}");
     }
 
     @Test
-    public void testDisableXmlElementMask() {
+    void testDisableXmlElementMask() {
         DefaultMaskingFormatter formatter = new DefaultMaskingFormatter(true, 
false, true);
         String answer
                 = formatter.format("key=value, myPassword=foo,\n 
myPassphrase=\"foo bar\", secretKey='!@#$%^&*() -+[]{};:'");
-        assertEquals("key=value, myPassword=xxxxx,\n myPassphrase=\"xxxxx\", 
secretKey='xxxxx'", answer);
+        assertThat(answer).isEqualTo("key=value, myPassword=xxxxx,\n 
myPassphrase=\"xxxxx\", secretKey='xxxxx'");
 
         answer = formatter.format("<xmlPassword>\n foo bar 
\n</xmlPassword>\n<user password=\"asdf qwert\"/>");
-        assertEquals("<xmlPassword>\n foo bar \n</xmlPassword>\n<user 
password=\"xxxxx\"/>", answer);
+        assertThat(answer).isEqualTo("<xmlPassword>\n foo bar 
\n</xmlPassword>\n<user password=\"xxxxx\"/>");
 
         answer = formatter.format(
                 "{\"key\" : \"value\", \"Password\":\"foo\", \"Passphrase\" : 
\"foo bar\", \"SecretKey\" : \"!@#$%^&*() -+[]{};:'\"}");
-        assertEquals(
-                "{\"key\" : \"value\", \"Password\":\"xxxxx\", \"Passphrase\" 
: \"xxxxx\", \"SecretKey\" : \"xxxxx\"}",
-                answer);
+        assertThat(answer).isEqualTo(
+                "{\"key\" : \"value\", \"Password\":\"xxxxx\", \"Passphrase\" 
: \"xxxxx\", \"SecretKey\" : \"xxxxx\"}");
     }
 
     @Test
-    public void testDisableJsonMask() {
+    void testDisableJsonMask() {
         DefaultMaskingFormatter formatter = new DefaultMaskingFormatter(true, 
true, false);
         String answer
                 = formatter.format("key=value, myPassword=foo,\n 
myPassphrase=\"foo bar\", secretKey='!@#$%^&*() -+[]{};:'");
-        assertEquals("key=value, myPassword=xxxxx,\n myPassphrase=\"xxxxx\", 
secretKey='xxxxx'", answer);
+        assertThat(answer).isEqualTo("key=value, myPassword=xxxxx,\n 
myPassphrase=\"xxxxx\", secretKey='xxxxx'");
 
         answer = formatter.format("<xmlPassword>\n foo bar 
\n</xmlPassword>\n<user password=\"asdf qwert\"/>");
-        assertEquals("<xmlPassword>\n xxxxx \n</xmlPassword>\n<user 
password=\"xxxxx\"/>", answer);
+        assertThat(answer).isEqualTo("<xmlPassword>\n xxxxx 
\n</xmlPassword>\n<user password=\"xxxxx\"/>");
 
         answer = formatter.format(
                 "{\"key\" : \"value\", \"My Password\":\"foo\", \"My 
SecretPassphrase\" : \"foo bar\", \"My SecretKey2\" : \"!@#$%^&*() 
-+[]{};:'\"}");
-        assertEquals(
-                "{\"key\" : \"value\", \"My Password\":\"foo\", \"My 
SecretPassphrase\" : \"foo bar\", \"My SecretKey2\" : \"!@#$%^&*() 
-+[]{};:'\"}",
-                answer);
+        assertThat(answer).isEqualTo(
+                "{\"key\" : \"value\", \"My Password\":\"foo\", \"My 
SecretPassphrase\" : \"foo bar\", \"My SecretKey2\" : \"!@#$%^&*() 
-+[]{};:'\"}");
     }
 
     @Test
-    public void testCustomMaskString() {
+    void testCustomMaskString() {
         DefaultMaskingFormatter formatter = new DefaultMaskingFormatter();
         formatter.setMaskString("**********");
         String answer
                 = formatter.format("key=value, myPassword=foo,\n 
myPassphrase=\"foo bar\", secretKey='!@#$%^&*() -+[]{};:'");
-        assertEquals("key=value, myPassword=**********,\n 
myPassphrase=\"**********\", secretKey='**********'", answer);
+        assertThat(answer).isEqualTo("key=value, myPassword=**********,\n 
myPassphrase=\"**********\", secretKey='**********'");
 
         answer = formatter.format("<xmlPassword>\n foo bar 
\n</xmlPassword>\n<user password=\"asdf qwert\"/>");
-        assertEquals("<xmlPassword>\n ********** \n</xmlPassword>\n<user 
password=\"**********\"/>", answer);
+        assertThat(answer).isEqualTo("<xmlPassword>\n ********** 
\n</xmlPassword>\n<user password=\"**********\"/>");
 
         answer = formatter.format(
                 "{\"key\" : \"value\", \"Password\":\"foo\", \"Passphrase\" : 
\"foo bar\", \"SecretKey\" : \"!@#$%^&*() -+[]{};:'\"}");
-        assertEquals(
-                "{\"key\" : \"value\", \"Password\":\"**********\", 
\"Passphrase\" : \"**********\", \"SecretKey\" : \"**********\"}",
-                answer);
+        assertThat(answer).isEqualTo(
+                "{\"key\" : \"value\", \"Password\":\"**********\", 
\"Passphrase\" : \"**********\", \"SecretKey\" : \"**********\"}");
     }
 
     @Test
-    public void testDifferentSensitiveKeys() {
+    void testDifferentSensitiveKeys() {
         DefaultMaskingFormatter formatter = new DefaultMaskingFormatter();
         String answer
                 = formatter.format("key=value, myAccessKey=foo,\n 
authkey=\"foo bar\", refreshtoken='!@#$%^&*() -+[]{};:'");
-        assertEquals("key=value, myAccessKey=xxxxx,\n authkey=\"xxxxx\", 
refreshtoken='xxxxx'", answer);
+        assertThat(answer).isEqualTo("key=value, myAccessKey=xxxxx,\n 
authkey=\"xxxxx\", refreshtoken='xxxxx'");
 
         answer = formatter.format("<subscribeKey>\n foo bar 
\n</subscribeKey>\n<user verificationCode=\"asdf qwert\"/>");
-        assertEquals("<subscribeKey>\n xxxxx \n</subscribeKey>\n<user 
verificationCode=\"xxxxx\"/>", answer);
+        assertThat(answer).isEqualTo("<subscribeKey>\n xxxxx 
\n</subscribeKey>\n<user verificationCode=\"xxxxx\"/>");
 
         answer = formatter.format(
                 "{\"key\" : \"value\", \"subscribeKey\":\"foo\", 
\"verificationCode\" : \"foo bar\", \"RefreshToken\" : \"!@#$%^&*() 
-+[]{};:'\"}");
-        assertEquals(
-                "{\"key\" : \"value\", \"subscribeKey\":\"xxxxx\", 
\"verificationCode\" : \"xxxxx\", \"RefreshToken\" : \"xxxxx\"}",
-                answer);
+        assertThat(answer).isEqualTo(
+                "{\"key\" : \"value\", \"subscribeKey\":\"xxxxx\", 
\"verificationCode\" : \"xxxxx\", \"RefreshToken\" : \"xxxxx\"}");
     }
 
     @Test
-    public void testCustomKeywords() {
+    void testCustomKeywords() {
         DefaultMaskingFormatter formatter = new DefaultMaskingFormatter();
         formatter.addKeyword("cheese");
         formatter.setMaskString("**********");
         String answer
                 = formatter.format(
                         "key=value, Cheese=gauda, myPassword=foo,\n 
myPassphrase=\"foo bar\", secretKey='!@#$%^&*() -+[]{};:'");
-        assertEquals(
-                "key=value, Cheese=**********, myPassword=**********,\n 
myPassphrase=\"**********\", secretKey='**********'",
-                answer);
+        assertThat(answer).isEqualTo(
+                "key=value, Cheese=**********, myPassword=**********,\n 
myPassphrase=\"**********\", secretKey='**********'");
 
         answer = formatter
                 .format("<chEEse>Gauda</chEEse><xmlPassword>\n foo bar 
\n</xmlPassword>\n<user password=\"asdf qwert\"/>");
-        assertEquals("<chEEse>**********</chEEse><xmlPassword>\n ********** 
\n</xmlPassword>\n<user password=\"**********\"/>",
-                answer);
+        assertThat(answer).isEqualTo(
+                "<chEEse>**********</chEEse><xmlPassword>\n ********** 
\n</xmlPassword>\n<user password=\"**********\"/>");
 
         answer = formatter.format(
                 "{\"key\" : \"value\", \"Cheese\": \"gauda\", 
\"Password\":\"foo\", \"Passphrase\" : \"foo bar\", \"SecretKey\" : 
\"!@#$%^&*() -+[]{};:'\"}");
-        assertEquals(
-                "{\"key\" : \"value\", \"Cheese\": \"**********\", 
\"Password\":\"**********\", \"Passphrase\" : \"**********\", \"SecretKey\" : 
\"**********\"}",
-                answer);
+        assertThat(answer).isEqualTo(
+                "{\"key\" : \"value\", \"Cheese\": \"**********\", 
\"Password\":\"**********\", \"Passphrase\" : \"**********\", \"SecretKey\" : 
\"**********\"}");
+    }
+
+    @Test
+    void formatMasksConnectionStringUserInfoCredentials() {
+        DefaultMaskingFormatter formatter = new DefaultMaskingFormatter();
+
+        assertThat(formatter.format("mongodb://user:pass@host:27017/db"))
+                .isEqualTo("mongodb://user:xxxxx@host:27017/db");
+        assertThat(formatter.format("amqp://admin:secret@broker:5672/vhost"))
+                .isEqualTo("amqp://admin:xxxxx@broker:5672/vhost");
+        assertThat(formatter.format("redis://:s3cret@redis:6379/0"))
+                .isEqualTo("redis://:xxxxx@redis:6379/0");
+        assertThat(formatter.format("uri=redis://default:s3cret@redis:6379/0 
password=visible"))
+                .isEqualTo("uri=redis://default:xxxxx@redis:6379/0 
password=xxxxx");
+    }
+
+    @Test
+    void formatMasksUserInfoInsideJsonWithoutSensitiveKeyName() {
+        DefaultMaskingFormatter formatter = new DefaultMaskingFormatter();
+        String answer = 
formatter.format("{\"url\":\"mongodb://user:secret@host/db\",\"name\":\"app\"}");
+        
assertThat(answer).isEqualTo("{\"url\":\"mongodb://user:xxxxx@host/db\",\"name\":\"app\"}");
+    }
+
+    @Test
+    void formatStillMasksQueryPasswordKeyValue() {
+        DefaultMaskingFormatter formatter = new DefaultMaskingFormatter();
+        // key=value masking stops at comma/quote; a lone ?password= form is 
still masked
+        assertThat(formatter.format("http://host/path?password=topsecret";))
+                .isEqualTo("http://host/path?password=xxxxx";);
+        assertThat(formatter.format("password=topsecret, user=alice"))
+                .isEqualTo("password=xxxxx, user=xxxxx");
+    }
+
+    @Test
+    void formatMasksPemPrivateKeyBlocks() {
+        DefaultMaskingFormatter formatter = new DefaultMaskingFormatter();
+        String source = """
+                -----BEGIN RSA PRIVATE KEY-----
+                MIIEowIBAAKCAQEA0Z3VS5JJcds3xfn
+                -----END RSA PRIVATE KEY-----
+                """;
+        String answer = formatter.format(source);
+        assertThat(answer)
+                .contains("-----BEGIN RSA PRIVATE KEY-----")
+                .contains("xxxxx")
+                .contains("-----END RSA PRIVATE KEY-----")
+                .doesNotContain("MIIEowIBAAKCAQEA0Z3VS5JJcds3xfn");
+    }
+
+    @Test
+    void formatDoesNotMaskCertificatesOrPublicKeys() {
+        DefaultMaskingFormatter formatter = new DefaultMaskingFormatter();
+        String certificate = """
+                -----BEGIN CERTIFICATE-----
+                MIIDXTCCAkWgAwIBAgIJAKHBj
+                -----END CERTIFICATE-----
+                """;
+        assertThat(formatter.format(certificate)).isEqualTo(certificate);
+
+        String publicKey = """
+                -----BEGIN PUBLIC KEY-----
+                MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBA
+                -----END PUBLIC KEY-----
+                """;
+        assertThat(formatter.format(publicKey)).isEqualTo(publicKey);
+    }
+
+    @Test
+    void formatMasksValueShapesEvenWhenKeyValueMaskDisabled() {
+        DefaultMaskingFormatter formatter = new DefaultMaskingFormatter(false, 
false, false);
+        assertThat(formatter.format("mongodb://user:pass@host/db"))
+                .isEqualTo("mongodb://user:xxxxx@host/db");
+        assertThat(formatter.format("""
+                -----BEGIN PRIVATE KEY-----
+                secret-bytes
+                -----END PRIVATE KEY-----
+                """)).doesNotContain("secret-bytes").contains("xxxxx");
+    }
+
+    @Test
+    void formatMasksValueShapesWithCustomMaskString() {
+        DefaultMaskingFormatter formatter = new DefaultMaskingFormatter();
+        formatter.setMaskString("***");
+        assertThat(formatter.format("amqp://admin:secret@broker/vhost"))
+                .isEqualTo("amqp://admin:***@broker/vhost");
+    }
+
+    @Test
+    void formatNullAndEmptyUnchanged() {
+        DefaultMaskingFormatter formatter = new DefaultMaskingFormatter();
+        assertThat(formatter.format(null)).isNull();
+        assertThat(formatter.format("")).isEmpty();
+    }
+
+    @Test
+    void formatPlainTextUnchanged() {
+        DefaultMaskingFormatter formatter = new DefaultMaskingFormatter();
+        assertThat(formatter.format("Hello World")).isEqualTo("Hello World");
+    }
+
+    @Test
+    void formatKeywordMaskingUsesLiteralMaskStringWithRegexMetacharacters() {
+        DefaultMaskingFormatter formatter = new DefaultMaskingFormatter();
+        formatter.setMaskString("$1");
+        // Without Matcher.quoteReplacement, $1 would expand to the regex 
capture group (the key prefix)
+        
assertThat(formatter.format("myPassword=foo")).isEqualTo("myPassword=$1");
+    }
+
+    @Test
+    void formatMasksValueShapesWhenKeywordsEmpty() {
+        DefaultMaskingFormatter formatter = new 
DefaultMaskingFormatter(Collections.emptySet(), true, true, true);
+        assertThat(formatter.format("mongodb://user:pass@host/db"))
+                .isEqualTo("mongodb://user:xxxxx@host/db");
+        
assertThat(formatter.format("password=visible")).isEqualTo("password=visible");
     }
 
 }
diff --git 
a/core/camel-support/src/main/java/org/apache/camel/support/processor/DefaultMaskingFormatter.java
 
b/core/camel-support/src/main/java/org/apache/camel/support/processor/DefaultMaskingFormatter.java
index f62af602f90b..eb8123deca57 100644
--- 
a/core/camel-support/src/main/java/org/apache/camel/support/processor/DefaultMaskingFormatter.java
+++ 
b/core/camel-support/src/main/java/org/apache/camel/support/processor/DefaultMaskingFormatter.java
@@ -19,6 +19,7 @@ package org.apache.camel.support.processor;
 import java.util.Locale;
 import java.util.Set;
 import java.util.TreeSet;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import org.apache.camel.CamelContext;
@@ -33,6 +34,12 @@ import org.slf4j.LoggerFactory;
  * <p>
  * By default all the known secret keys from {@link 
SensitiveUtils#getSensitiveKeys()} are used. Custom keywords can be
  * added with the {@link #addKeyword(String)} method.
+ * <p>
+ * In addition to name-based masking, this formatter also masks URI userinfo 
passwords
+ * ({@code scheme://user:password@host}) and PEM private-key blocks via
+ * {@link SensitiveUtils#maskSensitiveValueShapes(String, String)}. 
Value-shape masking runs even when the keyword set
+ * is empty (for example a custom formatter constructed with no sensitive key 
names), so embedded credentials in
+ * connection strings are still redacted.
  */
 public class DefaultMaskingFormatter implements MaskingFormatter {
 
@@ -113,28 +120,33 @@ public class DefaultMaskingFormatter implements 
MaskingFormatter {
 
     @Override
     public String format(String source) {
-        if (keywords == null || keywords.isEmpty()) {
+        if (source == null || source.isEmpty()) {
             return source;
         }
 
-        // xml,json or key=value pairs is the formats supported
-        boolean xml = maskXmlElement && source.startsWith("<");
-        boolean json = maskJson && !xml && (source.startsWith("{") || 
source.startsWith("["));
-
         String answer = source;
-        if (xml) {
-            answer = xmlElementMaskPattern.matcher(answer).replaceAll("$1" + 
maskString + "$3");
-            if (maskKeyValue) {
-                // used for the attributes in the XML tags
-                answer = keyValueMaskPattern.matcher(answer).replaceAll("$1" + 
maskString);
+        if (keywords != null && !keywords.isEmpty()) {
+            String replacement = Matcher.quoteReplacement(maskString);
+            // xml,json or key=value pairs is the formats supported
+            boolean xml = maskXmlElement && source.startsWith("<");
+            boolean json = maskJson && !xml && (source.startsWith("{") || 
source.startsWith("["));
+
+            if (xml) {
+                answer = xmlElementMaskPattern.matcher(answer).replaceAll("$1" 
+ replacement + "$3");
+                if (maskKeyValue) {
+                    // used for the attributes in the XML tags
+                    answer = 
keyValueMaskPattern.matcher(answer).replaceAll("$1" + replacement);
+                }
+            } else if (json) {
+                answer = 
jsonMaskPattern.matcher(answer).replaceAll("\"$1\"$2:$3\"" + replacement + 
"\"");
+            } else if (maskKeyValue) {
+                // key=value pairs
+                answer = keyValueMaskPattern.matcher(answer).replaceAll("$1" + 
replacement);
             }
-        } else if (json) {
-            answer = 
jsonMaskPattern.matcher(answer).replaceAll("\"$1\"$2:$3\"" + maskString + "\"");
-        } else if (maskKeyValue) {
-            // key=value paris
-            answer = keyValueMaskPattern.matcher(answer).replaceAll("$1" + 
maskString);
         }
 
+        // Value-shape secrets (URI userinfo passwords, PEM private keys) are 
not name-based
+        answer = SensitiveUtils.maskSensitiveValueShapes(answer, maskString);
         return answer;
     }
 
diff --git 
a/core/camel-util/src/main/java/org/apache/camel/util/SensitiveUtils.java 
b/core/camel-util/src/main/java/org/apache/camel/util/SensitiveUtils.java
index 18c0bf4b8435..c342d5019cba 100644
--- a/core/camel-util/src/main/java/org/apache/camel/util/SensitiveUtils.java
+++ b/core/camel-util/src/main/java/org/apache/camel/util/SensitiveUtils.java
@@ -21,9 +21,34 @@ import java.util.Collections;
 import java.util.HashSet;
 import java.util.Locale;
 import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
 
 public final class SensitiveUtils {
 
+    /**
+     * Matches URI userinfo credentials ({@code scheme://user:password@host} 
or {@code scheme://:password@host}) and
+     * captures the password as group 2. The scheme may contain {@code +} / 
{@code .} / {@code -} (e.g.
+     * {@code mongodb+srv}). Does not match user-only userinfo without a 
password ({@code scheme://user@host}).
+     * <p>
+     * Stricter than {@link URISupport}'s {@code USERINFO_PASSWORD} (used in 
{@link URISupport#sanitizeUri}): requires a
+     * scheme and a colon before the password. Used for free-text log masking. 
The user/password prefix uses a
+     * non-greedy match so passwords may contain {@code :} (aligned with 
{@link URISupport#sanitizeUri}).
+     * <p>
+     * Passwords must not contain a raw {@code @} (use percent-encoding such 
as {@code %40}); capture stops at the first
+     * {@code @} that ends userinfo.
+     */
+    private static final Pattern URI_USERINFO_PASSWORD_IN_TEXT
+            = 
Pattern.compile("([a-zA-Z][a-zA-Z0-9+.-]*://[^/@\\s\"']*?:)([^@\\s\"']+)(@)");
+
+    /**
+     * Matches PEM private-key blocks ({@code -----BEGIN ... PRIVATE KEY-----} 
… {@code -----END ... PRIVATE KEY-----})
+     * and captures the key body as group 2. Public keys and certificates are 
not matched.
+     */
+    private static final Pattern PEM_PRIVATE_KEY = Pattern.compile(
+            "(-----BEGIN [^-]*PRIVATE KEY-----\\s*)(.*?)(\\s*-----END 
[^-]*PRIVATE KEY-----)",
+            Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
+
     private static final Set<String> SENSITIVE_KEYS = 
Collections.unmodifiableSet(new HashSet<>(
             Arrays.asList(
                     // Generated by camel build tools - do NOT edit this list!
@@ -256,4 +281,61 @@ public final class SensitiveUtils {
         return SENSITIVE_KEYS.contains(text);
     }
 
+    /**
+     * Masks passwords embedded in URI userinfo ({@code 
scheme://user:password@host}) within free text. Complements
+     * name-based secret detection: the password has no {@code password=} key, 
so key/value maskers never see it.
+     * <p>
+     * Query-parameter forms such as {@code ?password=secret} are not handled 
here; use a key/value masker or
+     * {@link URISupport#sanitizeUri(String)} for those.
+     *
+     * @param  source the text that may contain connection-string style URIs
+     * @param  mask   the replacement string for the password (for example 
{@code xxxxx})
+     * @return        the source with userinfo passwords replaced, or the 
original source when null/empty or when mask
+     *                is null
+     */
+    public static String maskUserInfoCredentials(String source, String mask) {
+        if (source == null || source.isEmpty() || mask == null) {
+            return source;
+        }
+        if (!source.contains("://")) {
+            return source;
+        }
+        return URI_USERINFO_PASSWORD_IN_TEXT.matcher(source)
+                .replaceAll("$1" + Matcher.quoteReplacement(mask) + "$3");
+    }
+
+    /**
+     * Masks the body of PEM private-key blocks within free text, keeping the 
BEGIN/END markers. Public keys and
+     * certificates ({@code BEGIN PUBLIC KEY}, {@code BEGIN CERTIFICATE}) are 
left unchanged.
+     *
+     * @param  source the text that may contain PEM private-key material
+     * @param  mask   the replacement string for the key body (for example 
{@code xxxxx})
+     * @return        the source with private-key bodies replaced, or the 
original source when null/empty or when mask
+     *                is null
+     */
+    public static String maskPemPrivateKeyBlocks(String source, String mask) {
+        if (source == null || source.isEmpty() || mask == null) {
+            return source;
+        }
+        if (!StringHelper.containsIgnoreCase(source, "-----BEGIN")) {
+            return source;
+        }
+        return PEM_PRIVATE_KEY.matcher(source).replaceAll("$1" + 
Matcher.quoteReplacement(mask) + "$3");
+    }
+
+    /**
+     * Applies value-shape secret masking (URI userinfo passwords and PEM 
private-key bodies) that cannot be detected by
+     * sensitive key names alone.
+     *
+     * @param  source the text to mask
+     * @param  mask   the replacement string
+     * @return        the masked text, or the original source when null/empty 
or when mask is null
+     * @see           #maskUserInfoCredentials(String, String)
+     * @see           #maskPemPrivateKeyBlocks(String, String)
+     */
+    public static String maskSensitiveValueShapes(String source, String mask) {
+        String answer = maskUserInfoCredentials(source, mask);
+        return maskPemPrivateKeyBlocks(answer, mask);
+    }
+
 }
diff --git 
a/core/camel-util/src/test/java/org/apache/camel/util/SensitiveUtilsTest.java 
b/core/camel-util/src/test/java/org/apache/camel/util/SensitiveUtilsTest.java
index 8588c4b90249..2c551fb57a0c 100644
--- 
a/core/camel-util/src/test/java/org/apache/camel/util/SensitiveUtilsTest.java
+++ 
b/core/camel-util/src/test/java/org/apache/camel/util/SensitiveUtilsTest.java
@@ -19,37 +19,179 @@ package org.apache.camel.util;
 
 import org.junit.jupiter.api.Test;
 
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.assertj.core.api.Assertions.assertThat;
 
 class SensitiveUtilsTest {
 
     @Test
     void testContainsSensitive() {
-        assertTrue(SensitiveUtils.containsSensitive("accessKey"));
-        assertTrue(SensitiveUtils.containsSensitive("accesstoken"));
-        assertTrue(SensitiveUtils.containsSensitive("authorizationtoken"));
-        assertTrue(SensitiveUtils.containsSensitive("clientsecret"));
-        assertTrue(SensitiveUtils.containsSensitive("passphrase"));
-        assertTrue(SensitiveUtils.containsSensitive("password"));
-        assertTrue(SensitiveUtils.containsSensitive("sasljaasconfig"));
-        assertTrue(SensitiveUtils.containsSensitive("sasl-jaas-config"));
-        assertTrue(SensitiveUtils.containsSensitive("saslJaasConfig"));
-        assertTrue(SensitiveUtils.containsSensitive("secret"));
-        assertTrue(SensitiveUtils.containsSensitive("secretkey"));
-        assertTrue(SensitiveUtils.containsSensitive("secret-key"));
-        assertTrue(SensitiveUtils.containsSensitive("secretKey"));
-        assertTrue(SensitiveUtils.containsSensitive("secret-Key"));
-        assertTrue(SensitiveUtils.containsSensitive("access-key"));
-        assertTrue(SensitiveUtils.containsSensitive("accessKey"));
-        assertTrue(SensitiveUtils.containsSensitive("access-Key"));
-        assertTrue(SensitiveUtils.containsSensitive("client-secret"));
-        assertTrue(SensitiveUtils.containsSensitive("authorization-token"));
-        assertTrue(SensitiveUtils.containsSensitive("foo.bar.accessKey"));
-
-        assertFalse(SensitiveUtils.containsSensitive("foo.bar.accessKey."));
-        assertFalse(SensitiveUtils.containsSensitive("foo"));
-        assertFalse(SensitiveUtils.containsSensitive("bar"));
+        assertThat(SensitiveUtils.containsSensitive("accessKey")).isTrue();
+        assertThat(SensitiveUtils.containsSensitive("accesstoken")).isTrue();
+        
assertThat(SensitiveUtils.containsSensitive("authorizationtoken")).isTrue();
+        assertThat(SensitiveUtils.containsSensitive("clientsecret")).isTrue();
+        assertThat(SensitiveUtils.containsSensitive("passphrase")).isTrue();
+        assertThat(SensitiveUtils.containsSensitive("password")).isTrue();
+        
assertThat(SensitiveUtils.containsSensitive("sasljaasconfig")).isTrue();
+        
assertThat(SensitiveUtils.containsSensitive("sasl-jaas-config")).isTrue();
+        
assertThat(SensitiveUtils.containsSensitive("saslJaasConfig")).isTrue();
+        assertThat(SensitiveUtils.containsSensitive("secret")).isTrue();
+        assertThat(SensitiveUtils.containsSensitive("secretkey")).isTrue();
+        assertThat(SensitiveUtils.containsSensitive("secret-key")).isTrue();
+        assertThat(SensitiveUtils.containsSensitive("secretKey")).isTrue();
+        assertThat(SensitiveUtils.containsSensitive("secret-Key")).isTrue();
+        assertThat(SensitiveUtils.containsSensitive("access-key")).isTrue();
+        assertThat(SensitiveUtils.containsSensitive("accessKey")).isTrue();
+        assertThat(SensitiveUtils.containsSensitive("access-Key")).isTrue();
+        assertThat(SensitiveUtils.containsSensitive("client-secret")).isTrue();
+        
assertThat(SensitiveUtils.containsSensitive("authorization-token")).isTrue();
+        
assertThat(SensitiveUtils.containsSensitive("foo.bar.accessKey")).isTrue();
+
+        
assertThat(SensitiveUtils.containsSensitive("foo.bar.accessKey.")).isFalse();
+        assertThat(SensitiveUtils.containsSensitive("foo")).isFalse();
+        assertThat(SensitiveUtils.containsSensitive("bar")).isFalse();
+    }
+
+    @Test
+    void maskUserInfoCredentialsMasksPasswordInCommonSchemes() {
+        
assertThat(SensitiveUtils.maskUserInfoCredentials("mongodb://user:pass@host:27017/db",
 "xxxxx"))
+                .isEqualTo("mongodb://user:xxxxx@host:27017/db");
+        
assertThat(SensitiveUtils.maskUserInfoCredentials("mongodb+srv://user:p%40ss@cluster/db",
 "xxxxx"))
+                .isEqualTo("mongodb+srv://user:xxxxx@cluster/db");
+        
assertThat(SensitiveUtils.maskUserInfoCredentials("amqp://admin:secret@broker:5672/vhost",
 "xxxxx"))
+                .isEqualTo("amqp://admin:xxxxx@broker:5672/vhost");
+        
assertThat(SensitiveUtils.maskUserInfoCredentials("redis://default:s3cret@redis:6379/0",
 "xxxxx"))
+                .isEqualTo("redis://default:xxxxx@redis:6379/0");
+        
assertThat(SensitiveUtils.maskUserInfoCredentials("jdbc:mysql://dbuser:dbpass@localhost:3306/app",
 "xxxxx"))
+                .isEqualTo("jdbc:mysql://dbuser:xxxxx@localhost:3306/app");
+        
assertThat(SensitiveUtils.maskUserInfoCredentials("sftp://USERNAME:[email protected]";,
 "xxxxx"))
+                .isEqualTo("sftp://USERNAME:[email protected]";);
+        // Redis / some brokers allow empty username with password-only 
userinfo
+        
assertThat(SensitiveUtils.maskUserInfoCredentials("redis://:s3cret@redis:6379/0",
 "xxxxx"))
+                .isEqualTo("redis://:xxxxx@redis:6379/0");
+        
assertThat(SensitiveUtils.maskUserInfoCredentials("http://user:pass:word@host/path";,
 "xxxxx"))
+                .isEqualTo("http://user:xxxxx@host/path";);
+    }
+
+    @Test
+    void maskValueShapeHelpersSkipRegexWhenMarkersAbsent() {
+        assertThat(SensitiveUtils.maskUserInfoCredentials("Hello World", 
"xxxxx")).isEqualTo("Hello World");
+        assertThat(SensitiveUtils.maskPemPrivateKeyBlocks("plain log line", 
"xxxxx")).isEqualTo("plain log line");
+        assertThat(SensitiveUtils.maskSensitiveValueShapes("Hello World", 
"xxxxx")).isEqualTo("Hello World");
+    }
+
+    @Test
+    void maskUserInfoCredentialsPreservesNonCredentialUris() {
+        
assertThat(SensitiveUtils.maskUserInfoCredentials("https://example.com/path";, 
"xxxxx"))
+                .isEqualTo("https://example.com/path";);
+        
assertThat(SensitiveUtils.maskUserInfoCredentials("mongodb://user@host:27017/db",
 "xxxxx"))
+                .isEqualTo("mongodb://user@host:27017/db");
+        
assertThat(SensitiveUtils.maskUserInfoCredentials("mailto:[email protected]";, 
"xxxxx"))
+                .isEqualTo("mailto:[email protected]";);
+        assertThat(SensitiveUtils.maskUserInfoCredentials("see 
http://example.com/path:foo for details", "xxxxx"))
+                .isEqualTo("see http://example.com/path:foo for details");
+    }
+
+    @Test
+    void maskUserInfoCredentialsMasksMultipleAndEmbedded() {
+        String source = "primary=mongodb://u1:p1@h1/db 
secondary=amqp://u2:p2@h2/v";
+        assertThat(SensitiveUtils.maskUserInfoCredentials(source, "xxxxx"))
+                .isEqualTo("primary=mongodb://u1:xxxxx@h1/db 
secondary=amqp://u2:xxxxx@h2/v");
+
+        assertThat(SensitiveUtils.maskUserInfoCredentials(
+                "{\"url\":\"mongodb://user:secret@host/db\"}", "xxxxx"))
+                .isEqualTo("{\"url\":\"mongodb://user:xxxxx@host/db\"}");
+    }
+
+    @Test
+    void maskUserInfoCredentialsHandlesNullEmptyAndSpecialMask() {
+        assertThat(SensitiveUtils.maskUserInfoCredentials(null, 
"xxxxx")).isNull();
+        assertThat(SensitiveUtils.maskUserInfoCredentials("", 
"xxxxx")).isEmpty();
+        
assertThat(SensitiveUtils.maskUserInfoCredentials("mongodb://u:p@h/db", null))
+                .isEqualTo("mongodb://u:p@h/db");
+        
assertThat(SensitiveUtils.maskUserInfoCredentials("mongodb://u:p@h/db", 
"hide$me"))
+                .isEqualTo("mongodb://u:hide$me@h/db");
+    }
+
+    @Test
+    void maskPemPrivateKeyBlocksMasksBodiesAndKeepsMarkers() {
+        String rsa = """
+                -----BEGIN RSA PRIVATE KEY-----
+                MIIEowIBAAKCAQEA0Z3VS5JJcds3xfn/ygWyF6PZFEw4N8AQ
+                -----END RSA PRIVATE KEY-----
+                """;
+        assertThat(SensitiveUtils.maskPemPrivateKeyBlocks(rsa, "xxxxx"))
+                .isEqualTo("""
+                        -----BEGIN RSA PRIVATE KEY-----
+                        xxxxx
+                        -----END RSA PRIVATE KEY-----
+                        """);
+
+        String pkcs8 = """
+                -----BEGIN PRIVATE KEY-----
+                MGACAQAwBQYDK2VwBCIEIJ+keyMaterialHere
+                -----END PRIVATE KEY-----
+                """;
+        assertThat(SensitiveUtils.maskPemPrivateKeyBlocks(pkcs8, "xxxxx"))
+                .contains("-----BEGIN PRIVATE KEY-----")
+                .contains("xxxxx")
+                .contains("-----END PRIVATE KEY-----")
+                .doesNotContain("keyMaterialHere");
+
+        String ec = """
+                -----BEGIN EC PRIVATE KEY-----
+                MHQCAQEEISecretEcMaterial
+                -----END EC PRIVATE KEY-----
+                """;
+        assertThat(SensitiveUtils.maskPemPrivateKeyBlocks(ec, 
"xxxxx")).doesNotContain("SecretEcMaterial");
+
+        String encrypted = """
+                -----BEGIN ENCRYPTED PRIVATE KEY-----
+                encrypted-key-material
+                -----END ENCRYPTED PRIVATE KEY-----
+                """;
+        assertThat(SensitiveUtils.maskPemPrivateKeyBlocks(encrypted, "xxxxx"))
+                .doesNotContain("encrypted-key-material")
+                .contains("-----BEGIN ENCRYPTED PRIVATE KEY-----")
+                .contains("xxxxx");
     }
 
+    @Test
+    void maskPemPrivateKeyBlocksLeavesPublicMaterialAlone() {
+        String certificate = """
+                -----BEGIN CERTIFICATE-----
+                MIIDXTCCAkWgAwIBAgIJAKHBj
+                -----END CERTIFICATE-----
+                """;
+        assertThat(SensitiveUtils.maskPemPrivateKeyBlocks(certificate, 
"xxxxx")).isEqualTo(certificate);
+
+        String publicKey = """
+                -----BEGIN PUBLIC KEY-----
+                MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBA
+                -----END PUBLIC KEY-----
+                """;
+        assertThat(SensitiveUtils.maskPemPrivateKeyBlocks(publicKey, 
"xxxxx")).isEqualTo(publicKey);
+
+        String lowerCaseHeader = """
+                -----begin rsa private key-----
+                secretEcMaterial
+                -----end rsa private key-----
+                """;
+        assertThat(SensitiveUtils.maskPemPrivateKeyBlocks(lowerCaseHeader, 
"xxxxx")).doesNotContain("secretEcMaterial");
+    }
+
+    @Test
+    void maskSensitiveValueShapesCombinesUserInfoAndPem() {
+        String source = """
+                uri=mongodb://user:pass@host/db
+                -----BEGIN PRIVATE KEY-----
+                secret-key-bytes
+                -----END PRIVATE KEY-----
+                """;
+        String masked = SensitiveUtils.maskSensitiveValueShapes(source, 
"xxxxx");
+        assertThat(masked)
+                .contains("mongodb://user:xxxxx@host/db")
+                .contains("xxxxx")
+                .doesNotContain("pass@")
+                .doesNotContain("secret-key-bytes");
+    }
 }
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
index 6a63aff5955f..e955af4b076e 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_22.adoc
@@ -819,6 +819,24 @@ without being secrets — `adjustAuthorization`, 
`jwtAuthorizationType`, `proxyA
 have their values shown as `xxxxxx` in sanitized URIs. This is a cosmetic 
change to sanitized output only and
 does not affect the actual endpoint configuration.
 
+=== camel-support - DefaultMaskingFormatter masks URI userinfo and PEM private 
keys
+
+`DefaultMaskingFormatter` (used when `logMask=true`, and by tooling that 
reuses the same formatter) previously
+masked secrets only when a known sensitive *key name* appeared in key/value, 
XML or JSON text. Two credential
+shapes that have no such key are now also masked:
+
+* URI userinfo passwords such as `mongodb://user:pass@host/db` or 
`amqp://admin:secret@broker/vhost`
+  (the `:pass` / `:secret` portion is replaced; the username and host remain).
+* PEM private-key blocks (`-----BEGIN … PRIVATE KEY-----` … `-----END … 
PRIVATE KEY-----`); the key body is
+  replaced while the BEGIN/END markers are kept. Certificates and public keys 
are not masked.
+
+Helpers for these value shapes live on `org.apache.camel.util.SensitiveUtils`
+(`maskUserInfoCredentials`, `maskPemPrivateKeyBlocks`, 
`maskSensitiveValueShapes`). This is a cosmetic change
+to masked/logged output only and does not affect endpoint configuration.
+
+If you use `DefaultMaskingFormatter` with an empty keyword set, `format()` 
previously returned the source unchanged;
+it now still applies URI userinfo and PEM private-key masking on the text.
+
 === camel-a2a - push notification webhooks pinned to the validated address
 
 Push notifications are now delivered with Apache HttpClient 5 instead of 
`java.net.http.HttpClient`, so the request


Reply via email to