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 cec2df58992f camel-clickup: validate the webhook signature against the
UTF-8 body (#29696)
cec2df58992f is described below
commit cec2df58992f6c2e23a5af874c387f60e81d9e6a
Author: dhairya05 <[email protected]>
AuthorDate: Thu Nov 12 23:39:29 2026 +0530
camel-clickup: validate the webhook signature against the UTF-8 body
(#29696)
---
.../clickup/service/ClickUpWebhookService.java | 19 ++++--
.../apache/camel/component/clickup/util/Utils.java | 20 ++++++-
.../camel/component/clickup/util/UtilsTest.java | 69 ++++++++++++++++++++++
3 files changed, 100 insertions(+), 8 deletions(-)
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 ca0786a9cca5..769abcccac76 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
@@ -18,6 +18,8 @@ package org.apache.camel.component.clickup.service;
import java.io.IOException;
import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
import java.util.Optional;
import java.util.Set;
@@ -80,23 +82,30 @@ public class ClickUpWebhookService {
throw new RuntimeException("The webhook HTTP request doesn't
contain the 'x-signature' header.");
}
- String messageBody;
+ byte[] messageBytes;
try (InputStream body = message.getBody(InputStream.class)) {
- messageBody = new String(body.readAllBytes());
+ // the signature covers the bytes as they arrived: decoding and
re-encoding them first would turn a
+ // malformed sequence into U+FFFD and change what is hashed
+ messageBytes = body.readAllBytes();
- LOG.debug("Message body {} to be validated against the given
signature {}", messageBody, signature);
+ LOG.debug("Message body {} to be validated against the given
signature {}",
+ new String(messageBytes, StandardCharsets.UTF_8),
signature);
body.reset();
} catch (IOException e) {
throw new RuntimeException(e);
}
- String calculatedSignature = Utils.computeMessageHMAC(messageBody,
sharedSecret);
+ String messageBody = new String(messageBytes, StandardCharsets.UTF_8);
+ String calculatedSignature = Utils.computeMessageHMAC(messageBytes,
sharedSecret);
signature = signature.toLowerCase();
calculatedSignature = calculatedSignature.toLowerCase();
- boolean signatureMatches = calculatedSignature.equals(signature);
+ // constant time, so that a wrong signature cannot be refined one
character at a time
+ boolean signatureMatches = MessageDigest.isEqual(
+ calculatedSignature.getBytes(StandardCharsets.US_ASCII),
+ signature.getBytes(StandardCharsets.US_ASCII));
if (!signatureMatches) {
LOG.debug("The message signature {} does not match {}.",
signature, calculatedSignature);
diff --git
a/components/camel-clickup/src/main/java/org/apache/camel/component/clickup/util/Utils.java
b/components/camel-clickup/src/main/java/org/apache/camel/component/clickup/util/Utils.java
index d66accfe654d..5b6ce08e0705 100644
---
a/components/camel-clickup/src/main/java/org/apache/camel/component/clickup/util/Utils.java
+++
b/components/camel-clickup/src/main/java/org/apache/camel/component/clickup/util/Utils.java
@@ -16,6 +16,7 @@
*/
package org.apache.camel.component.clickup.util;
+import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
@@ -35,11 +36,23 @@ public class Utils {
private static final String HMAC_HASHING_ALGORITHM = "HmacSHA256";
public static String computeMessageHMAC(String message, String key) {
+ // ClickUp signs the raw request body, so neither side of the HMAC may
depend on the platform default charset
+ return computeMessageHMAC(message.getBytes(StandardCharsets.UTF_8),
key);
+ }
+
+ /**
+ * Computes the HMAC of the message exactly as it arrived, without
decoding and re-encoding it first.
+ *
+ * @param message the raw message bytes, as they were received
+ * @param key the webhook's shared secret
+ * @return the HMAC as an upper-case hexadecimal string
+ */
+ public static String computeMessageHMAC(byte[] message, String key) {
try {
Mac mac = Mac.getInstance(HMAC_HASHING_ALGORITHM);
- mac.init(new SecretKeySpec(key.getBytes(),
HMAC_HASHING_ALGORITHM));
+ mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8),
HMAC_HASHING_ALGORITHM));
- byte[] hash = mac.doFinal(message.getBytes());
+ byte[] hash = mac.doFinal(message);
return DatatypeConverter.printHexBinary(hash);
} catch (NoSuchAlgorithmException e) {
@@ -47,7 +60,8 @@ public class Utils {
throw new RuntimeCamelException(e);
} catch (InvalidKeyException e) {
- LOG.debug("This exception should never occur: the provided key is
not valid. {}", key);
+ // the key is the webhook's shared secret, so it must not be logged
+ LOG.debug("This exception should never occur: the provided key is
not valid.");
throw new RuntimeCamelException(e);
}
diff --git
a/components/camel-clickup/src/test/java/org/apache/camel/component/clickup/util/UtilsTest.java
b/components/camel-clickup/src/test/java/org/apache/camel/component/clickup/util/UtilsTest.java
new file mode 100644
index 000000000000..4d0c7db5742c
--- /dev/null
+++
b/components/camel-clickup/src/test/java/org/apache/camel/component/clickup/util/UtilsTest.java
@@ -0,0 +1,69 @@
+/*
+ * 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.util;
+
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * The webhook signature has to be reproducible on any JVM: ClickUp computes
it over the raw request body, so the
+ * expected values below are HMAC-SHA256 over UTF-8 bytes.
+ */
+public class UtilsTest {
+
+ @Test
+ public void shouldComputeTheHmacOfAnAsciiMessage() {
+ assertThat(Utils.computeMessageHMAC("payload", "secret"))
+
.isEqualTo("B82FCB791ACEC57859B989B430A826488CE2E479FDF92326BD0A2E8375A42BA4");
+ }
+
+ @Test
+ public void shouldComputeTheHmacOverUtf8Bytes() {
+ // "secret" and "payload" with a diaeresis and a tilde: neither is
representable in US-ASCII, and both
+ // encode differently in ISO-8859-1
+ final String key = "s\u00ebcret";
+ final String message = "p\u00e3yload";
+
+ assertThat(Utils.computeMessageHMAC(message, key))
+
.isEqualTo("3F449438582E13522059F2FD70C99DDF4BEB084815B88E184F487E3F0F627656");
+ }
+
+ @Test
+ public void shouldHashTheRawBytesAsGiven() {
+ final String key = "s\u00ebcret";
+ final String message = "p\u00e3yload";
+
+ // the byte[] overload is what the webhook check uses, so for valid
UTF-8 it has to agree with the String one
+
assertThat(Utils.computeMessageHMAC(message.getBytes(StandardCharsets.UTF_8),
key))
+ .isEqualTo(Utils.computeMessageHMAC(message, key));
+ }
+
+ @Test
+ public void shouldHashABodyThatIsNotValidUtf8() {
+ final String key = "secret";
+ // a lone 0x80 continuation byte: decoding the body to a String first
turns it into U+FFFD and changes the
+ // hash, which is why the check has to work on the bytes as they
arrived
+ final byte[] message = { 'a', (byte) 0x80, 'b' };
+
+ assertThat(Utils.computeMessageHMAC(message, key))
+ .isNotEqualTo(Utils.computeMessageHMAC(new String(message,
StandardCharsets.UTF_8), key));
+ }
+
+}