This is an automated email from the ASF dual-hosted git repository. oscerd pushed a commit to branch backport/24034-to-camel-4.18.x in repository https://gitbox.apache.org/repos/asf/camel.git
commit 6954a9b34ac2fab01fdddb4cb64ef46ea654fc92 Author: Andrea Cosentino <[email protected]> AuthorDate: Wed Jun 17 12:59:38 2026 +0200 CAMEL-23762: camel-whatsapp - support X-Hub-Signature-256 verification of inbound webhook payloads Backport of #24034 to camel-4.18.x. Adds the opt-in webhookSecret option; when set, inbound webhook event callbacks with a missing or invalid X-Hub-Signature-256 HMAC-SHA256 signature are rejected with HTTP 403 (constant-time comparison). When unset, behaviour is unchanged. Generated files regenerated on the 4.18.x branch. The upgrade-guide note for this new option is added separately on main (4_18 guide). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> --- .../apache/camel/catalog/components/whatsapp.json | 3 +- .../whatsapp/WhatsAppEndpointConfigurer.java | 6 ++ .../whatsapp/WhatsAppEndpointUriFactory.java | 6 +- .../apache/camel/component/whatsapp/whatsapp.json | 3 +- .../component/whatsapp/WhatsAppConfiguration.java | 15 +++++ .../whatsapp/WhatsAppWebhookProcessor.java | 47 +++++++++++++-- .../whatsapp/WhatsAppWebhookSignatureTest.java | 69 ++++++++++++++++++++++ .../dsl/WhatsAppEndpointBuilderFactory.java | 18 ++++++ 8 files changed, 159 insertions(+), 8 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/whatsapp.json b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/whatsapp.json index 8cb671870764..bdf1cd94fb26 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/whatsapp.json +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/components/whatsapp.json @@ -46,6 +46,7 @@ "webhookPath": { "index": 5, "kind": "parameter", "displayName": "Webhook Path", "group": "advanced", "label": "advanced", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "webhook", "configurationClass": "org.apache.camel.component.whatsapp.WhatsAppConfiguration", "configurationField": "configuration", "description": "Webhook path" }, "webhookVerifyToken": { "index": 6, "kind": "parameter", "displayName": "Webhook Verify Token", "group": "advanced", "label": "advanced", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.whatsapp.WhatsAppConfiguration", "configurationField": "configuration", "description": "Webhook verify token" }, "whatsappService": { "index": 7, "kind": "parameter", "displayName": "Whatsapp Service", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.component.whatsapp.WhatsAppService", "deprecated": false, "autowired": false, "secret": false, "description": "WhatsApp service implementation" }, - "authorizationToken": { "index": 8, "kind": "parameter", "displayName": "Authorization Token", "group": "security", "label": "security", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "configurationClass": "org.apache.camel.component.whatsapp.WhatsAppConfiguration", "configurationField": "configuration", "description": "The authorization access token taken from whatsapp-business dashb [...] + "authorizationToken": { "index": 8, "kind": "parameter", "displayName": "Authorization Token", "group": "security", "label": "security", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "configurationClass": "org.apache.camel.component.whatsapp.WhatsAppConfiguration", "configurationField": "configuration", "description": "The authorization access token taken from whatsapp-business dashb [...] + "webhookSecret": { "index": 9, "kind": "parameter", "displayName": "Webhook Secret", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "configurationClass": "org.apache.camel.component.whatsapp.WhatsAppConfiguration", "configurationField": "configuration", "description": "The app secret used to verify the X-Hub-Signature-256 signature of inbound webhook event payloads [...] } } diff --git a/components/camel-whatsapp/src/generated/java/org/apache/camel/component/whatsapp/WhatsAppEndpointConfigurer.java b/components/camel-whatsapp/src/generated/java/org/apache/camel/component/whatsapp/WhatsAppEndpointConfigurer.java index 91f688d6ee09..d4d2aee025a5 100644 --- a/components/camel-whatsapp/src/generated/java/org/apache/camel/component/whatsapp/WhatsAppEndpointConfigurer.java +++ b/components/camel-whatsapp/src/generated/java/org/apache/camel/component/whatsapp/WhatsAppEndpointConfigurer.java @@ -35,6 +35,8 @@ public class WhatsAppEndpointConfigurer extends PropertyConfigurerSupport implem case "lazyStartProducer": target.setLazyStartProducer(property(camelContext, boolean.class, value)); return true; case "webhookpath": case "webhookPath": target.getConfiguration().setWebhookPath(property(camelContext, java.lang.String.class, value)); return true; + case "webhooksecret": + case "webhookSecret": target.getConfiguration().setWebhookSecret(property(camelContext, java.lang.String.class, value)); return true; case "webhookverifytoken": case "webhookVerifyToken": target.getConfiguration().setWebhookVerifyToken(property(camelContext, java.lang.String.class, value)); return true; case "whatsappservice": @@ -58,6 +60,8 @@ public class WhatsAppEndpointConfigurer extends PropertyConfigurerSupport implem case "lazyStartProducer": return boolean.class; case "webhookpath": case "webhookPath": return java.lang.String.class; + case "webhooksecret": + case "webhookSecret": return java.lang.String.class; case "webhookverifytoken": case "webhookVerifyToken": return java.lang.String.class; case "whatsappservice": @@ -82,6 +86,8 @@ public class WhatsAppEndpointConfigurer extends PropertyConfigurerSupport implem case "lazyStartProducer": return target.isLazyStartProducer(); case "webhookpath": case "webhookPath": return target.getConfiguration().getWebhookPath(); + case "webhooksecret": + case "webhookSecret": return target.getConfiguration().getWebhookSecret(); case "webhookverifytoken": case "webhookVerifyToken": return target.getConfiguration().getWebhookVerifyToken(); case "whatsappservice": diff --git a/components/camel-whatsapp/src/generated/java/org/apache/camel/component/whatsapp/WhatsAppEndpointUriFactory.java b/components/camel-whatsapp/src/generated/java/org/apache/camel/component/whatsapp/WhatsAppEndpointUriFactory.java index 6208beaeb6bc..48e36f6d02c5 100644 --- a/components/camel-whatsapp/src/generated/java/org/apache/camel/component/whatsapp/WhatsAppEndpointUriFactory.java +++ b/components/camel-whatsapp/src/generated/java/org/apache/camel/component/whatsapp/WhatsAppEndpointUriFactory.java @@ -23,7 +23,7 @@ public class WhatsAppEndpointUriFactory extends org.apache.camel.support.compone private static final Set<String> SECRET_PROPERTY_NAMES; private static final Map<String, String> MULTI_VALUE_PREFIXES; static { - Set<String> props = new HashSet<>(9); + Set<String> props = new HashSet<>(10); props.add("apiVersion"); props.add("authorizationToken"); props.add("baseUri"); @@ -31,11 +31,13 @@ public class WhatsAppEndpointUriFactory extends org.apache.camel.support.compone props.add("lazyStartProducer"); props.add("phoneNumberId"); props.add("webhookPath"); + props.add("webhookSecret"); props.add("webhookVerifyToken"); props.add("whatsappService"); PROPERTY_NAMES = Collections.unmodifiableSet(props); - Set<String> secretProps = new HashSet<>(1); + Set<String> secretProps = new HashSet<>(2); secretProps.add("authorizationToken"); + secretProps.add("webhookSecret"); SECRET_PROPERTY_NAMES = Collections.unmodifiableSet(secretProps); MULTI_VALUE_PREFIXES = Collections.emptyMap(); } diff --git a/components/camel-whatsapp/src/generated/resources/META-INF/org/apache/camel/component/whatsapp/whatsapp.json b/components/camel-whatsapp/src/generated/resources/META-INF/org/apache/camel/component/whatsapp/whatsapp.json index 8cb671870764..bdf1cd94fb26 100644 --- a/components/camel-whatsapp/src/generated/resources/META-INF/org/apache/camel/component/whatsapp/whatsapp.json +++ b/components/camel-whatsapp/src/generated/resources/META-INF/org/apache/camel/component/whatsapp/whatsapp.json @@ -46,6 +46,7 @@ "webhookPath": { "index": 5, "kind": "parameter", "displayName": "Webhook Path", "group": "advanced", "label": "advanced", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "defaultValue": "webhook", "configurationClass": "org.apache.camel.component.whatsapp.WhatsAppConfiguration", "configurationField": "configuration", "description": "Webhook path" }, "webhookVerifyToken": { "index": 6, "kind": "parameter", "displayName": "Webhook Verify Token", "group": "advanced", "label": "advanced", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "configurationClass": "org.apache.camel.component.whatsapp.WhatsAppConfiguration", "configurationField": "configuration", "description": "Webhook verify token" }, "whatsappService": { "index": 7, "kind": "parameter", "displayName": "Whatsapp Service", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.component.whatsapp.WhatsAppService", "deprecated": false, "autowired": false, "secret": false, "description": "WhatsApp service implementation" }, - "authorizationToken": { "index": 8, "kind": "parameter", "displayName": "Authorization Token", "group": "security", "label": "security", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "configurationClass": "org.apache.camel.component.whatsapp.WhatsAppConfiguration", "configurationField": "configuration", "description": "The authorization access token taken from whatsapp-business dashb [...] + "authorizationToken": { "index": 8, "kind": "parameter", "displayName": "Authorization Token", "group": "security", "label": "security", "required": true, "type": "string", "javaType": "java.lang.String", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": true, "configurationClass": "org.apache.camel.component.whatsapp.WhatsAppConfiguration", "configurationField": "configuration", "description": "The authorization access token taken from whatsapp-business dashb [...] + "webhookSecret": { "index": 9, "kind": "parameter", "displayName": "Webhook Secret", "group": "security", "label": "security", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": true, "configurationClass": "org.apache.camel.component.whatsapp.WhatsAppConfiguration", "configurationField": "configuration", "description": "The app secret used to verify the X-Hub-Signature-256 signature of inbound webhook event payloads [...] } } diff --git a/components/camel-whatsapp/src/main/java/org/apache/camel/component/whatsapp/WhatsAppConfiguration.java b/components/camel-whatsapp/src/main/java/org/apache/camel/component/whatsapp/WhatsAppConfiguration.java index a239a5853591..419ddcd8ba92 100644 --- a/components/camel-whatsapp/src/main/java/org/apache/camel/component/whatsapp/WhatsAppConfiguration.java +++ b/components/camel-whatsapp/src/main/java/org/apache/camel/component/whatsapp/WhatsAppConfiguration.java @@ -46,6 +46,13 @@ public class WhatsAppConfiguration { @UriParam(description = "Webhook path", label = "advanced", defaultValue = "webhook") private String webhookPath = "camel-whatsapp/webhook"; + @UriParam(description = "The app secret used to verify the X-Hub-Signature-256 signature of inbound webhook event" + + " payloads (from the Meta/WhatsApp app dashboard). When set, event callbacks with a missing" + + " or invalid signature are rejected with HTTP 403; when not set, no signature verification" + + " is performed.", + label = "security", secret = true) + private String webhookSecret; + public WhatsAppConfiguration() { } @@ -97,6 +104,14 @@ public class WhatsAppConfiguration { this.webhookPath = webhookPath; } + public String getWebhookSecret() { + return webhookSecret; + } + + public void setWebhookSecret(String webhookSecret) { + this.webhookSecret = webhookSecret; + } + @Override public String toString() { return "WhatsAppConfiguration{" + "authorizationToken='" + authorizationToken + '\'' + ", baseUri='" + baseUri + '\'' diff --git a/components/camel-whatsapp/src/main/java/org/apache/camel/component/whatsapp/WhatsAppWebhookProcessor.java b/components/camel-whatsapp/src/main/java/org/apache/camel/component/whatsapp/WhatsAppWebhookProcessor.java index e9158ae9cd22..a120824636cf 100644 --- a/components/camel-whatsapp/src/main/java/org/apache/camel/component/whatsapp/WhatsAppWebhookProcessor.java +++ b/components/camel-whatsapp/src/main/java/org/apache/camel/component/whatsapp/WhatsAppWebhookProcessor.java @@ -18,9 +18,16 @@ package org.apache.camel.component.whatsapp; import java.io.IOException; import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; import java.util.HashMap; +import java.util.HexFormat; import java.util.Map; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + import org.apache.camel.AsyncCallback; import org.apache.camel.AsyncProcessor; import org.apache.camel.Exchange; @@ -36,6 +43,7 @@ public class WhatsAppWebhookProcessor extends AsyncProcessorSupport { private static final String MODE_QUERY_PARAM = "hub.mode"; private static final String VERIFY_TOKEN_QUERY_PARAM = "hub.verify_token"; private static final String CHALLENGE_QUERY_PARAM = "hub.challenge"; + private static final String HUB_SIGNATURE_HEADER = "X-Hub-Signature-256"; private final WhatsAppConfiguration configuration; @@ -72,15 +80,26 @@ public class WhatsAppWebhookProcessor extends AsyncProcessorSupport { exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 400); } } else { - InputStream body = exchange.getIn().getBody(InputStream.class); - - try { - content = new String(body.readAllBytes()); + byte[] requestBody; + try (InputStream body = exchange.getIn().getBody(InputStream.class)) { + requestBody = body.readAllBytes(); } catch (IOException e) { exchange.setException(e); callback.done(true); return true; } + + // When a webhook secret is configured, verify the X-Hub-Signature-256 payload signature + String webhookSecret = configuration.getWebhookSecret(); + if (webhookSecret != null && !isValidSignature(requestBody, + exchange.getIn().getHeader(HUB_SIGNATURE_HEADER, String.class), webhookSecret)) { + LOG.warn("Rejecting WhatsApp webhook event with missing or invalid {} signature", HUB_SIGNATURE_HEADER); + exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 403); + callback.done(true); + return true; + } + + content = new String(requestBody, StandardCharsets.UTF_8); } exchange.getMessage().setBody(content); @@ -92,6 +111,26 @@ public class WhatsAppWebhookProcessor extends AsyncProcessorSupport { }); } + /** + * Verifies the {@code X-Hub-Signature-256} header against an HMAC-SHA256 of the raw request body keyed by the + * configured webhook secret, using a constant-time comparison. + */ + static boolean isValidSignature(byte[] payload, String signatureHeader, String secret) { + if (signatureHeader == null) { + return false; + } + try { + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); + String expected = "sha256=" + HexFormat.of().formatHex(mac.doFinal(payload)); + return MessageDigest.isEqual( + expected.getBytes(StandardCharsets.UTF_8), + signatureHeader.getBytes(StandardCharsets.UTF_8)); + } catch (GeneralSecurityException e) { + return false; + } + } + private Map<String, String> parseQueryParam(Exchange exchange) { Map<String, String> queryParams = new HashMap<>(); diff --git a/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookSignatureTest.java b/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookSignatureTest.java new file mode 100644 index 000000000000..ab10a87db1fd --- /dev/null +++ b/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookSignatureTest.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.whatsapp; + +import java.nio.charset.StandardCharsets; +import java.util.HexFormat; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class WhatsAppWebhookSignatureTest { + + private static final String SECRET = "my-app-secret"; + + private final byte[] payload = "{\"object\":\"whatsapp_business_account\"}".getBytes(StandardCharsets.UTF_8); + + @Test + void validSignatureAccepted() throws Exception { + assertTrue(WhatsAppWebhookProcessor.isValidSignature(payload, sign(payload, SECRET), SECRET)); + } + + @Test + void invalidSignatureRejected() { + assertFalse(WhatsAppWebhookProcessor.isValidSignature(payload, "sha256=deadbeef", SECRET)); + } + + @Test + void missingSignatureRejected() { + assertFalse(WhatsAppWebhookProcessor.isValidSignature(payload, null, SECRET)); + } + + @Test + void tamperedPayloadRejected() throws Exception { + String signature = sign(payload, SECRET); + byte[] tampered = "{\"object\":\"evil\"}".getBytes(StandardCharsets.UTF_8); + assertFalse(WhatsAppWebhookProcessor.isValidSignature(tampered, signature, SECRET)); + } + + @Test + void wrongSecretRejected() throws Exception { + String signature = sign(payload, "another-secret"); + assertFalse(WhatsAppWebhookProcessor.isValidSignature(payload, signature, SECRET)); + } + + private static String sign(byte[] payload, String secret) throws Exception { + Mac mac = Mac.getInstance("HmacSHA256"); + mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256")); + return "sha256=" + HexFormat.of().formatHex(mac.doFinal(payload)); + } +} diff --git a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/WhatsAppEndpointBuilderFactory.java b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/WhatsAppEndpointBuilderFactory.java index c0353b4e6b12..e658a6112be8 100644 --- a/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/WhatsAppEndpointBuilderFactory.java +++ b/dsl/camel-endpointdsl/src/generated/java/org/apache/camel/builder/endpoint/dsl/WhatsAppEndpointBuilderFactory.java @@ -60,6 +60,24 @@ public interface WhatsAppEndpointBuilderFactory { doSetProperty("authorizationToken", authorizationToken); return this; } + /** + * The app secret used to verify the X-Hub-Signature-256 signature of + * inbound webhook event payloads (from the Meta/WhatsApp app + * dashboard). When set, event callbacks with a missing or invalid + * signature are rejected with HTTP 403; when not set, no signature + * verification is performed. + * + * The option is a: <code>java.lang.String</code> type. + * + * Group: security + * + * @param webhookSecret the value to set + * @return the dsl builder + */ + default WhatsAppEndpointBuilder webhookSecret(String webhookSecret) { + doSetProperty("webhookSecret", webhookSecret); + return this; + } } /**
