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 644ff8dadb28 CAMEL-26276: camel-clickup - do not log the webhook 
signatures, and mask the credentials in toString() (#29809)
644ff8dadb28 is described below

commit 644ff8dadb2825dfab9aeec5e76c53a5364e2b69
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Nov 13 16:39:38 2026 +0100

    CAMEL-26276: camel-clickup - do not log the webhook signatures, and mask 
the credentials in toString() (#29809)
    
    The webhook consumer logs the body it validates and whether the check 
passed at DEBUG, but no longer the signature
    from the X-Signature header nor the one it calculates. The 
InvalidMessageSignatureException of a failed check no
    longer carries the calculated signature: getCalculatedSignature() and the 
constructor that takes it are deprecated.
    
    The toString() of the ClickUp configuration and of the webhook masks the 
credentials.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../component/clickup/ClickUpConfiguration.java    |  11 +-
 .../clickup/InvalidMessageSignatureException.java  |  15 +++
 .../camel/component/clickup/model/Webhook.java     |   9 +-
 .../clickup/service/ClickUpWebhookService.java     |  12 +-
 .../component/clickup/ClickUpToStringTest.java     |  60 ++++++++++
 .../clickup/ClickUpWebhookSignatureLogTest.java    | 127 +++++++++++++++++++++
 .../ROOT/pages/camel-4x-upgrade-guide-4_18.adoc    |   5 +
 .../ROOT/pages/camel-4x-upgrade-guide-4_22.adoc    |   5 +
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |   5 +
 9 files changed, 241 insertions(+), 8 deletions(-)

diff --git 
a/components/camel-clickup/src/main/java/org/apache/camel/component/clickup/ClickUpConfiguration.java
 
b/components/camel-clickup/src/main/java/org/apache/camel/component/clickup/ClickUpConfiguration.java
index 01707df67851..fb1892d5e193 100644
--- 
a/components/camel-clickup/src/main/java/org/apache/camel/component/clickup/ClickUpConfiguration.java
+++ 
b/components/camel-clickup/src/main/java/org/apache/camel/component/clickup/ClickUpConfiguration.java
@@ -93,10 +93,17 @@ public class ClickUpConfiguration {
     public String toString() {
         return "ClickUpConfiguration{" +
                "workspaceId=" + workspaceId +
-               ", webhookSecret='" + webhookSecret + '\'' +
-               ", authorizationToken='" + authorizationToken + '\'' +
+               ", webhookSecret='" + mask(webhookSecret) + '\'' +
+               ", authorizationToken='" + mask(authorizationToken) + '\'' +
                ", baseUrl='" + baseUrl + '\'' +
                '}';
     }
 
+    /**
+     * Masks a credential so that logging this configuration cannot leak it. 
{@code xxxxxx} is the mask Camel uses when
+     * it sanitizes a URI, and whether the value was set at all is still 
visible.
+     */
+    private static String mask(Object secret) {
+        return secret != null ? "xxxxxx" : "null";
+    }
 }
diff --git 
a/components/camel-clickup/src/main/java/org/apache/camel/component/clickup/InvalidMessageSignatureException.java
 
b/components/camel-clickup/src/main/java/org/apache/camel/component/clickup/InvalidMessageSignatureException.java
index 53e2ac4dcbd1..6130b063eaee 100644
--- 
a/components/camel-clickup/src/main/java/org/apache/camel/component/clickup/InvalidMessageSignatureException.java
+++ 
b/components/camel-clickup/src/main/java/org/apache/camel/component/clickup/InvalidMessageSignatureException.java
@@ -22,6 +22,17 @@ public class InvalidMessageSignatureException extends 
RuntimeException {
     private final String messageSignature;
     private final String calculatedSignature;
 
+    public InvalidMessageSignatureException(String message, String 
messageSignature) {
+        this.message = message;
+        this.messageSignature = messageSignature;
+        this.calculatedSignature = null;
+    }
+
+    /**
+     * @deprecated the calculated signature is the valid signature of the 
message, so it should not travel with the
+     *             exception; use {@link 
#InvalidMessageSignatureException(String, String)}
+     */
+    @Deprecated(since = "4.24.0")
     public InvalidMessageSignatureException(String message, String 
messageSignature, String calculatedSignature) {
         this.message = message;
         this.messageSignature = messageSignature;
@@ -37,6 +48,10 @@ public class InvalidMessageSignatureException extends 
RuntimeException {
         return messageSignature;
     }
 
+    /**
+     * @deprecated the webhook consumer no longer sets the calculated 
signature, so this returns null for its exceptions
+     */
+    @Deprecated(since = "4.24.0")
     public String getCalculatedSignature() {
         return calculatedSignature;
     }
diff --git 
a/components/camel-clickup/src/main/java/org/apache/camel/component/clickup/model/Webhook.java
 
b/components/camel-clickup/src/main/java/org/apache/camel/component/clickup/model/Webhook.java
index 38303d085762..e22d15c06ae9 100644
--- 
a/components/camel-clickup/src/main/java/org/apache/camel/component/clickup/model/Webhook.java
+++ 
b/components/camel-clickup/src/main/java/org/apache/camel/component/clickup/model/Webhook.java
@@ -184,8 +184,15 @@ public class Webhook implements Serializable {
                ", folderId='" + folderId + '\'' +
                ", spaceId='" + spaceId + '\'' +
                ", health=" + health +
-               ", secret='" + secret + '\'' +
+               ", secret='" + mask(secret) + '\'' +
                '}';
     }
 
+    /**
+     * Masks a credential so that logging a webhook cannot leak it. {@code 
xxxxxx} is the mask Camel uses when it
+     * sanitizes a URI, and whether the value was set at all is still visible.
+     */
+    private static String mask(Object secret) {
+        return secret != null ? "xxxxxx" : "null";
+    }
 }
diff --git 
a/components/camel-clickup/src/main/java/org/apache/camel/component/clickup/service/ClickUpWebhookService.java
 
b/components/camel-clickup/src/main/java/org/apache/camel/component/clickup/service/ClickUpWebhookService.java
index 769abcccac76..4d7a12c174cb 100644
--- 
a/components/camel-clickup/src/main/java/org/apache/camel/component/clickup/service/ClickUpWebhookService.java
+++ 
b/components/camel-clickup/src/main/java/org/apache/camel/component/clickup/service/ClickUpWebhookService.java
@@ -88,8 +88,9 @@ public class ClickUpWebhookService {
             // malformed sequence into U+FFFD and change what is hashed
             messageBytes = body.readAllBytes();
 
-            LOG.debug("Message body {} to be validated against the given 
signature {}",
-                    new String(messageBytes, StandardCharsets.UTF_8), 
signature);
+            // the signature authenticates the event, so it stays out of the 
log
+            LOG.debug("Message body {} to be validated against the given 
signature",
+                    new String(messageBytes, StandardCharsets.UTF_8));
 
             body.reset();
         } catch (IOException e) {
@@ -107,12 +108,13 @@ public class ClickUpWebhookService {
                 calculatedSignature.getBytes(StandardCharsets.US_ASCII),
                 signature.getBytes(StandardCharsets.US_ASCII));
         if (!signatureMatches) {
-            LOG.debug("The message signature {} does not match {}.", 
signature, calculatedSignature);
+            LOG.debug("The message signature does not match.");
 
-            throw new InvalidMessageSignatureException(messageBody, signature, 
calculatedSignature);
+            // the calculated signature is the valid one for this body, so it 
stays out of the exception too
+            throw new InvalidMessageSignatureException(messageBody, signature);
         }
 
-        LOG.debug("The message signature {} matches {}.", signature, 
calculatedSignature);
+        LOG.debug("The message signature matches.");
     }
 
 }
diff --git 
a/components/camel-clickup/src/test/java/org/apache/camel/component/clickup/ClickUpToStringTest.java
 
b/components/camel-clickup/src/test/java/org/apache/camel/component/clickup/ClickUpToStringTest.java
new file mode 100644
index 000000000000..099528e69437
--- /dev/null
+++ 
b/components/camel-clickup/src/test/java/org/apache/camel/component/clickup/ClickUpToStringTest.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.clickup;
+
+import org.apache.camel.component.clickup.model.Webhook;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The configuration and the webhook model are printed when logged, so their 
toString() masks the credentials.
+ */
+public class ClickUpToStringTest {
+
+    @Test
+    public void configurationMasksTheCredentials() {
+        ClickUpConfiguration configuration = new ClickUpConfiguration();
+        configuration.setWorkspaceId(42L);
+        configuration.setWebhookSecret("the-webhook-secret");
+        configuration.setAuthorizationToken("the-authorization-token");
+
+        assertThat(configuration.toString())
+                .contains("workspaceId=42")
+                .contains("webhookSecret='xxxxxx'")
+                .contains("authorizationToken='xxxxxx'")
+                .doesNotContain("the-webhook-secret")
+                .doesNotContain("the-authorization-token");
+    }
+
+    @Test
+    public void configurationShowsACredentialThatIsNotSet() {
+        assertThat(new ClickUpConfiguration().toString())
+                .contains("webhookSecret='null'")
+                .contains("authorizationToken='null'");
+    }
+
+    @Test
+    public void webhookMasksTheSecret() {
+        Webhook webhook = new Webhook();
+        webhook.setSecret("the-webhook-secret");
+
+        assertThat(webhook.toString())
+                .contains("secret='xxxxxx'")
+                .doesNotContain("the-webhook-secret");
+    }
+}
diff --git 
a/components/camel-clickup/src/test/java/org/apache/camel/component/clickup/ClickUpWebhookSignatureLogTest.java
 
b/components/camel-clickup/src/test/java/org/apache/camel/component/clickup/ClickUpWebhookSignatureLogTest.java
new file mode 100644
index 000000000000..242fb04087bd
--- /dev/null
+++ 
b/components/camel-clickup/src/test/java/org/apache/camel/component/clickup/ClickUpWebhookSignatureLogTest.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.component.clickup;
+
+import java.io.ByteArrayInputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Message;
+import org.apache.camel.component.clickup.service.ClickUpWebhookService;
+import org.apache.camel.component.clickup.util.Utils;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.support.DefaultMessage;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.Logger;
+import org.apache.logging.log4j.core.appender.AbstractAppender;
+import org.apache.logging.log4j.core.config.Configurator;
+import org.apache.logging.log4j.core.config.Property;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * The webhook signature check logs at DEBUG what it validates, but never a 
signature: the valid signature of an event
+ * stays out of the log and out of the exception of a failed check.
+ */
+public class ClickUpWebhookSignatureLogTest {
+
+    private static final String SECRET = "secret";
+    private static final String BODY = 
"{\"event\":\"taskTimeTrackedUpdated\"}";
+
+    private final CamelContext context = new DefaultCamelContext();
+    private final ClickUpWebhookService service = new 
ClickUpWebhookService(null);
+    private final LogEventCaptor captor = new LogEventCaptor();
+    private Logger logger;
+    private Level level;
+
+    @BeforeEach
+    public void captureDebugLog() {
+        logger = (Logger) LogManager.getLogger(ClickUpWebhookService.class);
+        level = logger.getLevel();
+        Configurator.setLevel(logger.getName(), Level.DEBUG);
+        logger.addAppender(captor);
+    }
+
+    @AfterEach
+    public void restoreLog() {
+        logger.removeAppender(captor);
+        Configurator.setLevel(logger.getName(), level);
+    }
+
+    @Test
+    public void validSignatureIsNotLogged() {
+        String signature = Utils.computeMessageHMAC(BODY, SECRET);
+
+        service.validateMessageSignature(message(signature), SECRET);
+
+        assertThat(debugMessages()).anyMatch(m -> m.contains("matches"));
+        assertThat(debugMessages()).anyMatch(m -> m.contains(BODY));
+        assertThat(debugMessages()).noneMatch(m -> 
m.toLowerCase().contains(signature.toLowerCase()));
+    }
+
+    @Test
+    public void 
calculatedSignatureIsNotLoggedNorInTheExceptionOfAFailedCheck() {
+        String validSignature = Utils.computeMessageHMAC(BODY, SECRET);
+        String wrongSignature = Utils.computeMessageHMAC(BODY, 
"another-secret");
+
+        assertThatThrownBy(() -> 
service.validateMessageSignature(message(wrongSignature), SECRET))
+                
.isInstanceOfSatisfying(InvalidMessageSignatureException.class, e -> {
+                    
assertThat(e.getMessageSignature()).isEqualToIgnoringCase(wrongSignature);
+                    assertThat(e.getCalculatedSignature()).isNull();
+                });
+
+        assertThat(debugMessages()).anyMatch(m -> m.contains("does not 
match"));
+        assertThat(debugMessages()).noneMatch(m -> 
m.toLowerCase().contains(validSignature.toLowerCase()));
+        assertThat(debugMessages()).noneMatch(m -> 
m.toLowerCase().contains(wrongSignature.toLowerCase()));
+    }
+
+    private Message message(String signature) {
+        Message message = new DefaultMessage(context);
+        message.setBody(new 
ByteArrayInputStream(BODY.getBytes(StandardCharsets.UTF_8)));
+        message.setHeader("x-signature", signature);
+        return message;
+    }
+
+    private List<String> debugMessages() {
+        return captor.events.stream()
+                .filter(event -> event.getLevel() == Level.DEBUG)
+                .map(event -> event.getMessage().getFormattedMessage())
+                .toList();
+    }
+
+    private static final class LogEventCaptor extends AbstractAppender {
+        private final List<LogEvent> events = new CopyOnWriteArrayList<>();
+
+        LogEventCaptor() {
+            super("ClickUpWebhookSignatureLogCaptor", null, null, false, 
Property.EMPTY_ARRAY);
+            start();
+        }
+
+        @Override
+        public void append(LogEvent event) {
+            events.add(event.toImmutable());
+        }
+    }
+}
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
index d7984f86549a..147ca4d39f69 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_18.adoc
@@ -72,6 +72,11 @@ Checkpoint failures from the timer are now logged at `WARN` 
level with the full
 at `DEBUG` level with the message only. Checkpoint failures triggered by batch 
size are still logged at
 `WARN`.
 
+=== camel-clickup - the webhook signature check no longer logs signatures
+
+The webhook consumer still logs the body it validates at `DEBUG`, but no 
longer the signature from the `X-Signature` header, nor the signature it 
calculates.
+The `InvalidMessageSignatureException` of a failed check no longer carries the 
calculated signature: `getCalculatedSignature()` returns `null` and is 
deprecated, as is the constructor that takes it.
+
 === camel-hazelcast - the hazelcast-seda consumer restores a transferred 
exchange only with transferExchange=true
 
 A `hazelcast-seda` consumer now restores the headers and exchange properties 
of an exchange that a producer with
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 34e38bc3c821..8560622e24b4 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
@@ -93,6 +93,11 @@ segment. A function name of several segments, such as 
`products/42` for a functi
 accepted. The `x-functions-key` header is only added to an `https` URL on an 
`azurewebsites.net` host. Valid
 configurations are not affected.
 
+=== camel-clickup - the webhook signature check no longer logs signatures
+
+The webhook consumer still logs the body it validates at `DEBUG`, but no 
longer the signature from the `X-Signature` header, nor the signature it 
calculates.
+The `InvalidMessageSignatureException` of a failed check no longer carries the 
calculated signature: `getCalculatedSignature()` returns `null` and is 
deprecated, as is the constructor that takes it.
+
 === camel-hazelcast - the hazelcast-seda consumer restores a transferred 
exchange only with transferExchange=true
 
 A `hazelcast-seda` consumer now restores the headers and exchange properties 
of an exchange that a producer with
diff --git 
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index ec03b6400f3f..9580c105f0ef 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -32,6 +32,11 @@ camel.component.controlbus.allowed-languages = simple
 
 The `route` command (`controlbus:route?action=...`) is not affected.
 
+=== camel-clickup - the webhook signature check no longer logs signatures
+
+The webhook consumer still logs the body it validates at `DEBUG`, but no 
longer the signature from the `X-Signature` header, nor the signature it 
calculates.
+The `InvalidMessageSignatureException` of a failed check no longer carries the 
calculated signature: `getCalculatedSignature()` returns `null` and is 
deprecated, as is the constructor that takes it.
+
 === camel-observability-services - promoted to Stable
 
 The `camel-observability-services` component has been promoted from _Preview_ 
to _Stable_ support level.

Reply via email to