This is an automated email from the ASF dual-hosted git repository.
oscerd 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 84f265e8d1b3 CAMEL-24973: camel-util - Align URISupport.sanitizeUri
with the endpoint URI parser (#26803)
84f265e8d1b3 is described below
commit 84f265e8d1b3548fd03f0fd0474cf6c410e5e9e6
Author: Andrea Cosentino <[email protected]>
AuthorDate: Fri Sep 25 11:19:50 2026 +0200
CAMEL-24973: camel-util - Align URISupport.sanitizeUri with the endpoint
URI parser (#26803)
- End RAW(...) and RAW{...} values the same way URIScanner does: at the
closing bracket followed by & or the end
- Use one userinfo masking rule for URISupport.sanitizeUri,
URISupport.sanitizePath and SensitiveUtils.maskUserInfoCredentials, applied to
every uri in the text
- Match sensitive keys ignoring dashes, as options can be configured in
dash case
- Apply the keywords from addSanitizeKeywords to every parameter and keep
the ones added earlier
- Mask sensitive values in the "Unknown parameters" error messages
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
Signed-off-by: Andrea Cosentino <[email protected]>
---
.../component/jetty12/JettyHttpComponent12.java | 4 +-
.../impl/engine/DefaultCamelContextExtension.java | 2 +-
.../apache/camel/impl/DefaultComponentTest.java | 13 ++
.../org/apache/camel/support/DefaultComponent.java | 2 +-
.../camel/support/ScheduledPollConsumer.java | 3 +-
.../java/org/apache/camel/util/SensitiveUtils.java | 137 ++++++++++++++++---
.../java/org/apache/camel/util/URISupport.java | 152 ++++++++++++++++-----
.../org/apache/camel/util/SensitiveUtilsTest.java | 24 ++++
.../java/org/apache/camel/util/URISupportTest.java | 113 +++++++++++++++
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 18 +++
10 files changed, 413 insertions(+), 55 deletions(-)
diff --git
a/components/camel-jetty/src/main/java/org/apache/camel/component/jetty12/JettyHttpComponent12.java
b/components/camel-jetty/src/main/java/org/apache/camel/component/jetty12/JettyHttpComponent12.java
index 9f9f069e338b..1090c561ab25 100644
---
a/components/camel-jetty/src/main/java/org/apache/camel/component/jetty12/JettyHttpComponent12.java
+++
b/components/camel-jetty/src/main/java/org/apache/camel/component/jetty12/JettyHttpComponent12.java
@@ -29,6 +29,7 @@ import org.apache.camel.component.jetty.JettyHttpEndpoint;
import org.apache.camel.spi.Metadata;
import org.apache.camel.spi.annotations.Component;
import org.apache.camel.support.PropertyBindingSupport;
+import org.apache.camel.util.URISupport;
import org.eclipse.jetty.server.ConnectionFactory;
import org.eclipse.jetty.server.ForwardedRequestCustomizer;
import org.eclipse.jetty.server.HttpConfiguration;
@@ -119,7 +120,8 @@ public class JettyHttpComponent12 extends
JettyHttpComponent {
throw new IllegalArgumentException(
"There are " + properties.size() + "
parameters that couldn't be set on the SocketConnector."
+ " Check the uri
if the parameters are spelt correctly and that they are properties of the
SelectChannelConnector."
- + " Unknown
parameters=[" + properties + "]");
+ + " Unknown
parameters=["
+ +
URISupport.sanitizeParameters(properties) + "]");
}
}
diff --git
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultCamelContextExtension.java
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultCamelContextExtension.java
index d2cce18c9a82..0be9ea6e0fb7 100644
---
a/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultCamelContextExtension.java
+++
b/core/camel-base-engine/src/main/java/org/apache/camel/impl/engine/DefaultCamelContextExtension.java
@@ -656,7 +656,7 @@ class DefaultCamelContextExtension implements
ExtendedCamelContext {
@Override
public void setAdditionalSensitiveKeywords(String
additionalSensitiveKeywords) {
this.additionalSensitiveKeywords = additionalSensitiveKeywords;
- // re-configure sensitive keywords asap so they take effect immediately
+ // add the keywords to the ones used when sanitizing uris, they apply
to the whole JVM and cannot be removed
URISupport.addSanitizeKeywords(additionalSensitiveKeywords);
}
diff --git
a/core/camel-core/src/test/java/org/apache/camel/impl/DefaultComponentTest.java
b/core/camel-core/src/test/java/org/apache/camel/impl/DefaultComponentTest.java
index 33c097196123..c86fc93826f5 100644
---
a/core/camel-core/src/test/java/org/apache/camel/impl/DefaultComponentTest.java
+++
b/core/camel-core/src/test/java/org/apache/camel/impl/DefaultComponentTest.java
@@ -26,15 +26,18 @@ import org.apache.camel.CamelContext;
import org.apache.camel.ContextTestSupport;
import org.apache.camel.Endpoint;
import org.apache.camel.NoSuchBeanException;
+import org.apache.camel.ResolveEndpointFailedException;
import org.apache.camel.TypeConversionException;
import org.apache.camel.spi.Registry;
import org.apache.camel.support.DefaultComponent;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Unit test for helper methods on the DefaultComponent.
@@ -270,6 +273,16 @@ public class DefaultComponentTest extends
ContextTestSupport {
assertEquals("camelContext must be specified", e.getMessage());
}
+ @Test
+ public void testUnknownParametersAreSanitized() {
+ ResolveEndpointFailedException e =
assertThrows(ResolveEndpointFailedException.class,
+ () ->
context.getEndpoint("timer:foo?privateKeyPassphrase=secret&foo=bar"),
+ "Should have thrown a ResolveEndpointFailedException");
+
+ assertTrue(e.getMessage().endsWith("Unknown parameters=[{foo=bar,
privateKeyPassphrase=xxxxxx}]"), e.getMessage());
+ assertFalse(e.getMessage().contains("secret"), e.getMessage());
+ }
+
@Override
protected Registry createCamelRegistry() throws Exception {
Date bean1 = new Date(10);
diff --git
a/core/camel-support/src/main/java/org/apache/camel/support/DefaultComponent.java
b/core/camel-support/src/main/java/org/apache/camel/support/DefaultComponent.java
index ad07db3178fa..cec14989670e 100644
---
a/core/camel-support/src/main/java/org/apache/camel/support/DefaultComponent.java
+++
b/core/camel-support/src/main/java/org/apache/camel/support/DefaultComponent.java
@@ -302,7 +302,7 @@ public abstract class DefaultComponent extends
ServiceSupport implements Compone
uri, "There are " + param.size()
+ " parameters that couldn't be set on the endpoint."
+ " Check the uri if the parameters are spelt
correctly and that they are properties of the endpoint."
- + " Unknown parameters=[" + param + "]");
+ + " Unknown parameters=[" +
URISupport.sanitizeParameters(param) + "]");
}
}
diff --git
a/core/camel-support/src/main/java/org/apache/camel/support/ScheduledPollConsumer.java
b/core/camel-support/src/main/java/org/apache/camel/support/ScheduledPollConsumer.java
index bcbfa60dfab8..8c765e9313b0 100644
---
a/core/camel-support/src/main/java/org/apache/camel/support/ScheduledPollConsumer.java
+++
b/core/camel-support/src/main/java/org/apache/camel/support/ScheduledPollConsumer.java
@@ -39,6 +39,7 @@ import org.apache.camel.spi.ScheduledPollConsumerScheduler;
import org.apache.camel.support.service.ServiceHelper;
import org.apache.camel.util.ObjectHelper;
import org.apache.camel.util.PropertiesHelper;
+import org.apache.camel.util.URISupport;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -668,7 +669,7 @@ public abstract class ScheduledPollConsumer extends
DefaultConsumer
getEndpoint(), "There are " + copy.size()
+ " scheduler parameters that couldn't
be set on the endpoint."
+ " Check the uri if the parameters are
spelt correctly and that they are properties of the endpoint."
- + " Unknown parameters=[" + copy + "]");
+ + " Unknown parameters=[" +
URISupport.sanitizeParameters(copy) + "]");
}
}
afterConfigureScheduler(scheduler, newScheduler);
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 7998a1691f29..f5a254b3ff6a 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
@@ -26,21 +26,6 @@ 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.
@@ -299,6 +284,11 @@ public final class SensitiveUtils {
* 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>
+ * The userinfo ends at the last {@code @} before the path or query, so a
password may contain {@code :} and
+ * {@code @}. A password with an unencoded {@code /} or {@code ?} is
masked up to the {@code @}, unless it contains
+ * something that reads as a query parameter ({@code ?key=} or {@code
&key=}). In free text a URI also ends at a
+ * whitespace or a quote. {@link URISupport#sanitizeUri(String)} masks
userinfo passwords with the same rule.
+ * <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.
*
@@ -311,11 +301,122 @@ public final class SensitiveUtils {
if (source == null || source.isEmpty() || mask == null) {
return source;
}
- if (!source.contains("://")) {
+ return maskUserInfo(source, mask, true);
+ }
+
+ /**
+ * Masks the userinfo password of every {@code scheme://user:password@} in
the source.
+ *
+ * @param source the source
+ * @param mask the replacement string for the password
+ * @param text whether the source is free text, where a whitespace or a
quote ends a URI
+ */
+ static String maskUserInfo(String source, String mask, boolean text) {
+ int idx = source.indexOf("://");
+ if (idx == -1) {
+ return source;
+ }
+ StringBuilder sb = null;
+ int copied = 0;
+ while (idx != -1) {
+ int next = idx + 3;
+ // there must be a scheme before ://
+ if (idx > 0 && isSchemeChar(source.charAt(idx - 1))) {
+ int[] password = passwordRange(source, idx + 3, text);
+ if (password != null) {
+ if (sb == null) {
+ sb = new StringBuilder(source.length());
+ }
+ sb.append(source, copied, password[0]).append(mask);
+ copied = password[1];
+ next = password[1];
+ }
+ }
+ idx = source.indexOf("://", next);
+ }
+ if (sb == null) {
return source;
}
- return URI_USERINFO_PASSWORD_IN_TEXT.matcher(source)
- .replaceAll("$1" + Matcher.quoteReplacement(mask) + "$3");
+ return sb.append(source, copied, source.length()).toString();
+ }
+
+ /**
+ * Masks the password of a URI path (without the scheme) that starts with
{@code user:password@}.
+ *
+ * @param path the path
+ * @param mask the replacement string for the password
+ */
+ static String maskPathUserInfo(String path, String mask) {
+ int[] password = passwordRange(path, 0, false);
+ if (password == null) {
+ return path;
+ }
+ return path.substring(0, password[0]) + mask +
path.substring(password[1]);
+ }
+
+ // the start and end of the password in the userinfo of the authority that
begins at start, or null if there is none
+ private static int[] passwordRange(String source, int start, boolean text)
{
+ int len = source.length();
+ // the user ends at the first colon
+ int colon = -1;
+ for (int i = start; i < len && colon == -1; i++) {
+ char ch = source.charAt(i);
+ if (ch == ':') {
+ colon = i;
+ } else if (ch == '/' || ch == '?' || text && isTextDelimiter(ch)) {
+ return null;
+ }
+ }
+ if (colon == -1) {
+ return null;
+ }
+ // the userinfo ends at the last @ before the path or query
+ int at = -1;
+ for (int i = colon + 1; i < len; i++) {
+ char ch = source.charAt(i);
+ if (ch == '@') {
+ at = i;
+ } else if (ch == '/' || ch == '?' || text && isTextDelimiter(ch)) {
+ break;
+ }
+ }
+ if (at == -1) {
+ // a password with an unencoded / or ? ends at the last @ before a
query parameter, the next uri,
+ // or (in free text) a whitespace or a quote
+ for (int i = colon + 1; i < len; i++) {
+ char ch = source.charAt(i);
+ if (ch == '@') {
+ at = i;
+ } else if ((ch == '?' || ch == '&') &&
isQueryParameter(source, i + 1)
+ || ch == ':' && source.startsWith("//", i + 1)
+ || text && isTextDelimiter(ch)) {
+ break;
+ }
+ }
+ }
+ return at == -1 ? null : new int[] { colon + 1, at };
+ }
+
+ // whether the text at the given index is a query parameter key followed
by =
+ private static boolean isQueryParameter(String source, int from) {
+ for (int i = from; i < source.length(); i++) {
+ char ch = source.charAt(i);
+ if (ch == '=') {
+ return i > from;
+ }
+ if (ch == '&' || ch == '?' || ch == '@' || ch == '/' || ch == ':'
|| Character.isWhitespace(ch)) {
+ return false;
+ }
+ }
+ return false;
+ }
+
+ private static boolean isSchemeChar(char ch) {
+ return Character.isLetterOrDigit(ch) || ch == '+' || ch == '.' || ch
== '-';
+ }
+
+ private static boolean isTextDelimiter(char ch) {
+ return Character.isWhitespace(ch) || ch == '"' || ch == '\'';
}
/**
diff --git
a/core/camel-util/src/main/java/org/apache/camel/util/URISupport.java
b/core/camel-util/src/main/java/org/apache/camel/util/URISupport.java
index 25c658967178..870528597371 100644
--- a/core/camel-util/src/main/java/org/apache/camel/util/URISupport.java
+++ b/core/camel-util/src/main/java/org/apache/camel/util/URISupport.java
@@ -27,6 +27,7 @@ import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
@@ -34,6 +35,7 @@ import java.util.Set;
import java.util.StringJoiner;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Function;
+import java.util.regex.Matcher;
import java.util.regex.Pattern;
import static org.apache.camel.util.CamelURIParser.URI_ALREADY_NORMALIZED;
@@ -51,31 +53,22 @@ public final class URISupport {
public static final char[] RAW_TOKEN_START = { '(', '{' };
public static final char[] RAW_TOKEN_END = { ')', '}' };
- @SuppressWarnings("RegExpUnnecessaryNonCapturingGroup")
- private static final String PRE_SECRETS_FORMAT =
"([?&][^=]*(?:%s)[^=]*)=(RAW(([{][^}]*[}])|([(][^)]*[)]))|[^&]*)";
+ // Match the key of a query parameter as first capture group (the value
starts after the = sign)
+ private static final Pattern QUERY_PARAMETER_KEY =
Pattern.compile("[?&]([^?&=]*)=");
- // Match any key-value pair in the URI query string whose key contains
- // "passphrase" or "password" or secret key (case-insensitive).
- // First capture group is the key, second is the value.
- private static final Pattern ALL_SECRETS
- =
Pattern.compile(PRE_SECRETS_FORMAT.formatted(SensitiveUtils.getSensitivePattern()),
- Pattern.CASE_INSENSITIVE);
+ // Match any of the sensitive keywords (such as passphrase, password or
secret key) in a query parameter key
+ private static final Pattern SENSITIVE_KEYWORDS
+ = Pattern.compile(SensitiveUtils.getSensitivePattern(),
Pattern.CASE_INSENSITIVE);
- // Match the user password in the URI as second capture group
- // (applies to URI with authority component and userinfo token in the form
- // "user:password").
- private static final Pattern USERINFO_PASSWORD =
Pattern.compile("(.*://.*?:)(.*)(@)");
-
- // Match the user password in the URI path as second capture group
- // (applies to URI path with authority component and userinfo token in the
- // form "user:password").
- private static final Pattern PATH_USERINFO_PASSWORD =
Pattern.compile("(.*?:)(.*)(@)");
+ // use xxxxxx as replacement as that works well with JMX also
+ private static final String MASK = "xxxxxx";
private static final Charset CHARSET = StandardCharsets.UTF_8;
private static final String EMPTY_QUERY_STRING = "";
- private static Pattern EXTRA_SECRETS;
+ // custom keywords (lower case and without dashes) added by
addSanitizeKeywords
+ private static volatile Set<String> extraKeywords = Set.of();
private URISupport() {
// Helper class
@@ -88,16 +81,26 @@ public final class URISupport {
* @param keywords keywords separated by comma
*/
public static synchronized void addSanitizeKeywords(String keywords) {
- StringJoiner pattern = new StringJoiner("|");
+ if (keywords == null) {
+ return;
+ }
+ Set<String> answer = new LinkedHashSet<>(extraKeywords);
for (String key : keywords.split(",")) {
- // skip existing keys
- key = key.toLowerCase(Locale.ROOT).trim();
- if (!SensitiveUtils.containsSensitive(key)) {
- pattern.add("\\Q" + key.toLowerCase(Locale.ROOT) + "\\E");
+ // keys are matched without dashes, the same way as the parameter
names
+ key = key.toLowerCase(Locale.ROOT).trim().replace("-", "");
+ // skip empty and existing keys
+ if (!key.isEmpty() && !SensitiveUtils.containsSensitive(key)) {
+ answer.add(key);
}
}
- EXTRA_SECRETS = Pattern.compile(PRE_SECRETS_FORMAT.formatted(pattern),
- Pattern.CASE_INSENSITIVE);
+ extraKeywords = Set.copyOf(answer);
+ }
+
+ /**
+ * Removes the keywords added by {@link #addSanitizeKeywords(String)}.
Only for testing.
+ */
+ static synchronized void resetSanitizeKeywords() {
+ extraKeywords = Set.of();
}
/**
@@ -106,21 +109,104 @@ public final class URISupport {
* @param uri The uri to sanitize.
* @return Returns null if the uri is null, otherwise the URI with the
passphrase, password or secretKey
* sanitized.
- * @see #ALL_SECRETS and #USERINFO_PASSWORD for the matched pattern
+ * @see SensitiveUtils#maskUserInfoCredentials(String, String) for
how the userinfo password is found
*/
public static String sanitizeUri(String uri) {
- // use xxxxx as replacement as that works well with JMX also
String sanitized = uri;
if (uri != null) {
- sanitized = ALL_SECRETS.matcher(sanitized).replaceAll("$1=xxxxxx");
- if (EXTRA_SECRETS != null) {
- sanitized =
EXTRA_SECRETS.matcher(sanitized).replaceFirst("$1=xxxxxx");
- }
- sanitized =
USERINFO_PASSWORD.matcher(sanitized).replaceFirst("$1xxxxxx$3");
+ sanitized = sanitizeQueryParameters(sanitized);
+ sanitized = SensitiveUtils.maskUserInfo(sanitized, MASK, false);
}
return sanitized;
}
+ /**
+ * Returns a copy of the parameters where the values of sensitive
parameters (such as passwords) are masked, using
+ * the same rules as {@link #sanitizeUri(String)}.
+ *
+ * @param parameters the parameters
+ * @return null if the parameters are null, otherwise a copy of
the parameters with the sensitive values
+ * masked
+ */
+ public static Map<String, Object> sanitizeParameters(Map<String, Object>
parameters) {
+ if (parameters == null) {
+ return null;
+ }
+ Map<String, Object> answer = new LinkedHashMap<>(parameters.size());
+ for (Map.Entry<String, Object> entry : parameters.entrySet()) {
+ Object value = entry.getValue();
+ answer.put(entry.getKey(), value != null &&
isSensitiveKey(entry.getKey()) ? MASK : value);
+ }
+ return answer;
+ }
+
+ private static String sanitizeQueryParameters(String uri) {
+ StringBuilder sb = null;
+ int copied = 0;
+ int pos = 0;
+ Matcher matcher = QUERY_PARAMETER_KEY.matcher(uri);
+ while (matcher.find(pos)) {
+ // the value starts after the = sign
+ pos = matcher.end();
+ if (isSensitiveKey(matcher.group(1))) {
+ int end = valueEnd(uri, pos);
+ if (sb == null) {
+ sb = new StringBuilder(uri.length());
+ }
+ sb.append(uri, copied, pos).append(MASK);
+ copied = end;
+ pos = end;
+ }
+ // otherwise continue searching from the start of the value, as it
can contain another uri
+ }
+ if (sb == null) {
+ return uri;
+ }
+ return sb.append(uri, copied, uri.length()).toString();
+ }
+
+ private static boolean isSensitiveKey(String key) {
+ // ignore dashes the same way as SensitiveUtils.containsSensitive, as
options can be configured in dash case
+ String text = key.toLowerCase(Locale.ROOT).replace("-", "");
+ if (SENSITIVE_KEYWORDS.matcher(text).find()) {
+ return true;
+ }
+ for (String keyword : extraKeywords) {
+ if (text.contains(keyword)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static int valueEnd(String uri, int start) {
+ int open = start + RAW_TOKEN_PREFIX.length();
+ if (uri.startsWith(RAW_TOKEN_PREFIX, start) && open < uri.length()) {
+ for (int i = 0; i < RAW_TOKEN_START.length; i++) {
+ if (uri.charAt(open) == RAW_TOKEN_START[i]) {
+ return rawValueEnd(uri, open + 1, RAW_TOKEN_END[i]);
+ }
+ }
+ }
+ int end = uri.indexOf('&', start);
+ return end != -1 ? end : uri.length();
+ }
+
+ private static int rawValueEnd(String uri, int from, char tokenEnd) {
+ // a RAW value ends at the closing bracket that is followed by & or
the end, the same way as URIScanner parses it
+ int last = -1;
+ for (int i = from; i < uri.length(); i++) {
+ if (uri.charAt(i) == tokenEnd) {
+ if (i + 1 == uri.length() || uri.charAt(i + 1) == '&') {
+ return i + 1;
+ }
+ last = i;
+ }
+ }
+ // no such bracket, which happens when the uri is part of a longer
text, so mask up to the last closing bracket
+ return last != -1 ? last + 1 : uri.length();
+ }
+
public static String textBlockToSingleLine(String uri) {
// Java 17 text blocks have new lines with optional white space
if (uri != null) {
@@ -159,7 +245,7 @@ public final class URISupport {
public static String sanitizePath(String path) {
String sanitized = path;
if (path != null) {
- sanitized =
PATH_USERINFO_PASSWORD.matcher(sanitized).replaceFirst("$1xxxxxx$3");
+ sanitized = SensitiveUtils.maskPathUserInfo(sanitized, MASK);
}
return sanitized;
}
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 eb5513cdc2ce..78a604198124 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
@@ -105,6 +105,30 @@ class SensitiveUtilsTest {
.isEqualTo("{\"url\":\"mongodb://user:xxxxx@host/db\"}");
}
+ @Test
+ void maskUserInfoCredentialsUsesTheSameRuleAsSanitizeUri() {
+ // the userinfo ends at the last @ before the path
+
assertThat(SensitiveUtils.maskUserInfoCredentials("ftp://joe:p@ss@host/in",
"xxxxx"))
+ .isEqualTo("ftp://joe:xxxxx@host/in");
+
assertThat(SensitiveUtils.maskUserInfoCredentials("ftp://joe:pa/ss@host/dir",
"xxxxx"))
+ .isEqualTo("ftp://joe:xxxxx@host/dir");
+ // the user may contain an @, such as an email address
+
assertThat(SensitiveUtils.maskUserInfoCredentials("ftp://[email protected]:pw@host/in",
"xxxxx"))
+ .isEqualTo("ftp://[email protected]:xxxxx@host/in");
+ // an @ in the query is not the end of a password
+
assertThat(SensitiveUtils.maskUserInfoCredentials("smtp://host:[email protected]",
"xxxxx"))
+ .isEqualTo("smtp://host:[email protected]");
+ assertThat(SensitiveUtils.maskUserInfoCredentials(
+
"ftp://joe:[email protected]/in?callbackUrl=http://cb.example.com:8080/x&[email protected]",
"xxxxx"))
+ .isEqualTo(
+
"ftp://joe:[email protected]/in?callbackUrl=http://cb.example.com:8080/x&[email protected]");
+ // in free text a whitespace ends the uri
+ assertThat(SensitiveUtils.maskUserInfoCredentials("Connecting to
ftp://joe:pw@host as [email protected]", "xxxxx"))
+ .isEqualTo("Connecting to ftp://joe:xxxxx@host as
[email protected]");
+ assertThat(SensitiveUtils.maskUserInfoCredentials("see
http://host:8080/x for [email protected]", "xxxxx"))
+ .isEqualTo("see http://host:8080/x for [email protected]");
+ }
+
@Test
void maskUserInfoCredentialsHandlesNullEmptyAndSpecialMask() {
assertThat(SensitiveUtils.maskUserInfoCredentials(null,
"xxxxx")).isNull();
diff --git
a/core/camel-util/src/test/java/org/apache/camel/util/URISupportTest.java
b/core/camel-util/src/test/java/org/apache/camel/util/URISupportTest.java
index 489bf6741f18..e4e2b517ce27 100644
--- a/core/camel-util/src/test/java/org/apache/camel/util/URISupportTest.java
+++ b/core/camel-util/src/test/java/org/apache/camel/util/URISupportTest.java
@@ -25,9 +25,11 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.entry;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
@@ -39,6 +41,12 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
public class URISupportTest {
+ @AfterEach
+ public void resetSanitizeKeywords() {
+ // the keywords added by some tests are global
+ URISupport.resetSanitizeKeywords();
+ }
+
@Test
public void testNormalizeEndpointUri() throws Exception {
String out1 =
URISupport.normalizeUri("smtp://localhost?username=davsclaus&password=secret");
@@ -766,4 +774,109 @@ public class URISupportTest {
assertEquals(expected, URISupport.sanitizeUri(uri1));
}
+ @Test
+ public void testSanitizeCustomKeysAppliedToAllParameters() {
+ URISupport.addSanitizeKeywords("pincode");
+ assertThat(URISupport.sanitizeUri("my:host?pincode=1111&pincode=2222"))
+ .isEqualTo("my:host?pincode=xxxxxx&pincode=xxxxxx");
+
+ // keywords added by an earlier call are kept
+ URISupport.addSanitizeKeywords("otp-code");
+
assertThat(URISupport.sanitizeUri("my:host?otpCode=123456&pincode=1111&otp-code=654321"))
+
.isEqualTo("my:host?otpCode=xxxxxx&pincode=xxxxxx&otp-code=xxxxxx");
+
+ // only built-in or empty keywords, which must not mask any other
parameter
+ URISupport.addSanitizeKeywords("password, ");
+ URISupport.addSanitizeKeywords(null);
+
assertThat(URISupport.sanitizeUri("my:host?foo=bar&timeout=5000")).isEqualTo("my:host?foo=bar&timeout=5000");
+ }
+
+ @Test
+ public void testSanitizeUriRawValueWithClosingBracket() {
+ // a RAW value ends at the closing bracket followed by & or the end,
the same way as when parsing the query
+
assertThat(URISupport.sanitizeUri("ftp://host/dir?password=RAW(se)cret)&binary=true"))
+ .isEqualTo("ftp://host/dir?password=xxxxxx&binary=true");
+
assertThat(URISupport.sanitizeUri("ftp://host/dir?password=RAW{se}cret}&binary=true"))
+ .isEqualTo("ftp://host/dir?password=xxxxxx&binary=true");
+
assertThat(URISupport.sanitizeUri("ftp://host/dir?binary=true&password=RAW()s3cr3t)"))
+ .isEqualTo("ftp://host/dir?binary=true&password=xxxxxx");
+
assertThat(URISupport.sanitizeUri("foo:bar?password=RAW(++?)w&rd)&serviceName=somechat"))
+ .isEqualTo("foo:bar?password=xxxxxx&serviceName=somechat");
+ }
+
+ @Test
+ public void testSanitizeUriRawValueInText() {
+ // the uri is part of a longer text, such as a route label, so the RAW
value is masked up to the last bracket
+
assertThat(URISupport.sanitizeUri("from[ftp://host/dir?password=RAW(se)cret)]"))
+ .isEqualTo("from[ftp://host/dir?password=xxxxxx]");
+
assertThat(URISupport.sanitizeUri("from[ftp://host/dir?password=RAW(secr...]"))
+ .isEqualTo("from[ftp://host/dir?password=xxxxxx");
+ }
+
+ @Test
+ public void testSanitizeUriWithUserInfoAndOtherUri() {
+ assertThat(URISupport.sanitizeUri(
+
"ftp://joe:[email protected]/in?callbackUrl=http://cb.example.com:8080/x&[email protected]"))
+ .isEqualTo(
+
"ftp://joe:[email protected]/in?callbackUrl=http://cb.example.com:8080/x&[email protected]");
+ // the userinfo ends at the last @ before the path
+
assertThat(URISupport.sanitizeUri("ftp://joe:p@[email protected]/in"))
+ .isEqualTo("ftp://joe:[email protected]/in");
+ // every uri in the text is sanitized
+ assertThat(URISupport.sanitizeUri("From[ftp://joe:secret1@host1/in] ->
To[sftp://bob:secret2@host2/out]"))
+ .isEqualTo("From[ftp://joe:xxxxxx@host1/in] ->
To[sftp://bob:xxxxxx@host2/out]");
+ // no userinfo
+
assertThat(URISupport.sanitizeUri("smtp://host:[email protected]")).isEqualTo("smtp://host:[email protected]");
+
assertThat(URISupport.sanitizeUri("ftp://joe@host:21/[email protected]"))
+ .isEqualTo("ftp://joe@host:21/[email protected]");
+ }
+
+ @Test
+ public void testSanitizeUriDashCaseKeys() {
+ assertThat(URISupport.sanitizeUri(
+
"my:host?access-key=A1&private-key=P2&connection-string=C3&sasl-jaas-config=J4&pass-phrase=P5&foo-bar=keep"))
+ .isEqualTo(
+
"my:host?access-key=xxxxxx&private-key=xxxxxx&connection-string=xxxxxx&sasl-jaas-config=xxxxxx&pass-phrase=xxxxxx&foo-bar=keep");
+ }
+
+ @Test
+ public void testSanitizeUriNestedUri() {
+
assertThat(URISupport.sanitizeUri("http://host/cb?url=http://other/path?token=abc&foo=bar"))
+
.isEqualTo("http://host/cb?url=http://other/path?token=xxxxxx&foo=bar");
+ }
+
+ @Test
+ public void testSanitizeParameters() {
+ Map<String, Object> parameters = new LinkedHashMap<>();
+ parameters.put("password", "secret");
+ parameters.put("private-key", "pk");
+ parameters.put("token", null);
+ parameters.put("foo", "bar");
+
+ assertThat(URISupport.sanitizeParameters(parameters))
+ .containsExactly(entry("password", "xxxxxx"),
entry("private-key", "xxxxxx"), entry("token", null),
+ entry("foo", "bar"));
+ // the given parameters are not changed
+ assertThat(parameters).containsEntry("password", "secret");
+ assertThat(URISupport.sanitizeParameters(null)).isNull();
+ }
+
+ @Test
+ public void testSanitizeUriWithUserInfoPasswordWithSlashOrQuestionMark() {
+
assertThat(URISupport.sanitizeUri("ftp://joe:pa/ss@host/dir")).isEqualTo("ftp://joe:xxxxxx@host/dir");
+
assertThat(URISupport.sanitizeUri("ftp://joe:pa?ss@host/dir?binary=true"))
+ .isEqualTo("ftp://joe:xxxxxx@host/dir?binary=true");
+
assertThat(URISupport.sanitizeUri("ftp://joe:p/a@b@host/dir")).isEqualTo("ftp://joe:xxxxxx@host/dir");
+ // a password that contains a query parameter (?key=value) cannot be
told apart from host:port?key=value@...
+ // (see smtp://host:[email protected]), so it is not masked
+
assertThat(URISupport.sanitizeUri("ftp://joe:p?a=b@host/dir")).isEqualTo("ftp://joe:p?a=b@host/dir");
+ }
+
+ @Test
+ public void testSanitizePathWithUserInfoAndOtherAt() {
+
assertThat(URISupport.sanitizePath("joe:secret@host/[email protected]"))
+ .isEqualTo("joe:xxxxxx@host/[email protected]");
+
assertThat(URISupport.sanitizePath("joe:pa/ss@host/dir")).isEqualTo("joe:xxxxxx@host/dir");
+ }
+
}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index 27a65d70d6c5..fdfc1d31cc00 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -613,6 +613,24 @@ structurally significant in Camel's own
`key=value&key=value` query syntax.
Code that asserts a literal, fully-normalized endpoint URI string containing
one of those characters in a
query value may need to update the expected string to the (now consistently)
unencoded form.
+=== camel-core - masking of sensitive values in endpoint URIs
+
+`URISupport.sanitizeUri()`, which masks secrets in endpoint URIs shown in
logs, events, JMX names and error
+messages, now masks some values it used to miss, so the masked form of a URI
can differ from earlier releases:
+
+* option names are matched ignoring dashes, so `access-key` is masked like
`accessKey`
+* a `RAW(...)` or `RAW{...}` value is masked up to the bracket that ends it,
also when the value contains that bracket
+* the userinfo password of every URI in a text is masked, not only the last one
+* the `Unknown parameters=[...]` message of an endpoint that cannot be created
masks the values of sensitive parameters
+
+The keywords added with `camel.main.additionalSensitiveKeywords` (or
`URISupport.addSanitizeKeywords()`) now apply to
+every matching parameter, not only the first one, and they are added to the
keywords configured earlier instead of
+replacing them. The keywords apply to the whole JVM and cannot be removed.
+
+`SensitiveUtils.maskUserInfoCredentials()`, used when masking log messages,
uses the same userinfo rule: the userinfo
+ends at the last `@` before the path, so a password may contain `@`, and an
`@` in the query of a URI is no longer
+taken as the end of a password.
+
=== camel-core - property placeholders in pollEnrich
Camel 4.22 stopped resolving property placeholders (`{{...}}`) on the
_per-message evaluated_