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