This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch camel-4.22.x
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 69eefc26c3b9541a61fbd51f0160616769a59539
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Nov 2 20:50:26 2026 +0100

    CAMEL-26164: camel-whatsapp - fix webhook query parsing and a missing HTTP 
method (#28940)
    
    … HTTP method
    
    The webhook verification request split each query pair on = and read the
    second part without a check, so an empty value or a pair without = failed
    with ArrayIndexOutOfBoundsException. Values were not URL-decoded, so an
    encoded hub.verify_token did not match and an encoded hub.challenge was
    echoed back encoded. The query is now parsed with indexOf, decoded once
    (from CamelHttpRawQuery when present, as netty-http sets CamelHttpQuery
    already decoded), and an invalid encoding gives 400.
    
    A request without the CamelHttpMethod header failed with a
    NullPointerException; it is now handled as an event (POST), and an event
    without a body is read as an empty body.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    (cherry picked from commit 9ee1e7cfade69c1dea913167643a97249007ee74)
---
 .../whatsapp/WhatsAppWebhookProcessor.java         |  44 ++++--
 .../whatsapp/WhatsAppWebhookProcessorTest.java     | 163 +++++++++++++++++++++
 .../component/whatsapp/WhatsAppWebhookTest.java    |  16 ++
 3 files changed, 213 insertions(+), 10 deletions(-)

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 a120824636cf..0a8bbe0e7e57 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,6 +18,7 @@ package org.apache.camel.component.whatsapp;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.net.URLDecoder;
 import java.nio.charset.StandardCharsets;
 import java.security.GeneralSecurityException;
 import java.security.MessageDigest;
@@ -58,9 +59,17 @@ public class WhatsAppWebhookProcessor extends 
AsyncProcessorSupport {
     public boolean process(Exchange exchange, AsyncCallback callback) {
         String content;
 
-        if 
("GET".equalsIgnoreCase(exchange.getIn().getHeader(Exchange.HTTP_METHOD).toString()))
 {
+        if 
("GET".equalsIgnoreCase(exchange.getIn().getHeader(Exchange.HTTP_METHOD, 
String.class))) {
             // Parse params from the webhook verification request
-            Map<String, String> queryParams = parseQueryParam(exchange);
+            Map<String, String> queryParams;
+            try {
+                queryParams = parseQueryParam(exchange);
+            } catch (IllegalArgumentException e) {
+                LOG.warn("Rejecting WhatsApp webhook verification request with 
an invalid query: {}", e.getMessage());
+                exchange.getMessage().setHeader(Exchange.HTTP_RESPONSE_CODE, 
400);
+                callback.done(true);
+                return true;
+            }
 
             String mode = queryParams.get(MODE_QUERY_PARAM);
             String token = queryParams.get(VERIFY_TOKEN_QUERY_PARAM);
@@ -82,7 +91,7 @@ public class WhatsAppWebhookProcessor extends 
AsyncProcessorSupport {
         } else {
             byte[] requestBody;
             try (InputStream body = 
exchange.getIn().getBody(InputStream.class)) {
-                requestBody = body.readAllBytes();
+                requestBody = body != null ? body.readAllBytes() : new byte[0];
             } catch (IOException e) {
                 exchange.setException(e);
                 callback.done(true);
@@ -131,15 +140,30 @@ public class WhatsAppWebhookProcessor extends 
AsyncProcessorSupport {
         }
     }
 
-    private Map<String, String> parseQueryParam(Exchange exchange) {
+    /**
+     * Parses the query parameters of the webhook verification request. The 
raw query is preferred (netty-http provides
+     * the decoded query in {@link Exchange#HTTP_QUERY}), and every key and 
value is URL decoded once. A parameter with
+     * an empty value or without {@code =} gets an empty value.
+     *
+     * @throws IllegalArgumentException if the query has an invalid URL 
encoding
+     */
+    static Map<String, String> parseQueryParam(Exchange exchange) {
         Map<String, String> queryParams = new HashMap<>();
 
-        if (exchange.getIn().getHeader(Exchange.HTTP_QUERY) != null) {
-            String[] pairs = 
exchange.getIn().getHeader(Exchange.HTTP_QUERY).toString().split("&");
-            for (String pair : pairs) {
-                String[] keyValuePair = pair.split("=");
-
-                queryParams.put(keyValuePair[0], keyValuePair[1]);
+        String query = exchange.getIn().getHeader(Exchange.HTTP_RAW_QUERY, 
String.class);
+        if (query == null) {
+            query = exchange.getIn().getHeader(Exchange.HTTP_QUERY, 
String.class);
+        }
+        if (query != null) {
+            for (String pair : query.split("&")) {
+                if (pair.isEmpty()) {
+                    continue;
+                }
+                int pos = pair.indexOf('=');
+                String key = pos == -1 ? pair : pair.substring(0, pos);
+                String value = pos == -1 ? "" : pair.substring(pos + 1);
+                queryParams.put(URLDecoder.decode(key, StandardCharsets.UTF_8),
+                        URLDecoder.decode(value, StandardCharsets.UTF_8));
             }
         }
 
diff --git 
a/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookProcessorTest.java
 
b/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookProcessorTest.java
new file mode 100644
index 000000000000..0ca0c7a1acd9
--- /dev/null
+++ 
b/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookProcessorTest.java
@@ -0,0 +1,163 @@
+/*
+ * 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.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.apache.camel.CamelContext;
+import org.apache.camel.Exchange;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.support.DefaultExchange;
+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;
+
+class WhatsAppWebhookProcessorTest {
+
+    private static final String VERIFY_TOKEN = "my token&more";
+
+    private CamelContext context;
+    private WhatsAppWebhookProcessor processor;
+    private final AtomicInteger calls = new AtomicInteger();
+
+    @BeforeEach
+    void setUp() {
+        context = new DefaultCamelContext();
+        WhatsAppConfiguration configuration = new WhatsAppConfiguration();
+        configuration.setWebhookVerifyToken(VERIFY_TOKEN);
+        processor = new WhatsAppWebhookProcessor(exchange -> 
calls.incrementAndGet(), configuration);
+        context.start();
+    }
+
+    @AfterEach
+    void tearDown() throws Exception {
+        context.close();
+    }
+
+    @Test
+    void emptyQueryValueAndMissingEqualsSign() {
+        Exchange exchange = verificationRequest(Exchange.HTTP_QUERY, 
"hub.challenge=&hub.mode=subscribe&flag");
+
+        assertThat(WhatsAppWebhookProcessor.parseQueryParam(exchange))
+                .containsExactlyInAnyOrderEntriesOf(Map.of("hub.challenge", 
"", "hub.mode", "subscribe", "flag", ""));
+
+        process(exchange);
+        // hub.verify_token is missing
+        
assertThat(exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isEqualTo(400);
+    }
+
+    @Test
+    void verifiedWithEmptyChallenge() {
+        Exchange exchange = verificationRequest(Exchange.HTTP_QUERY,
+                
"hub.mode=subscribe&hub.verify_token=my%20token%26more&hub.challenge=");
+
+        process(exchange);
+
+        
assertThat(exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isNull();
+        assertThat(exchange.getMessage().getBody()).isEqualTo("");
+        assertThat(calls).hasValue(1);
+    }
+
+    @Test
+    void encodedTokenAndChallengeAreDecoded() {
+        Exchange exchange = verificationRequest(Exchange.HTTP_QUERY,
+                
"hub.mode=subscribe&hub.verify_token=my+token%26more&hub.challenge=a%3Db%20c");
+
+        process(exchange);
+
+        
assertThat(exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isNull();
+        assertThat(exchange.getMessage().getBody()).isEqualTo("a=b c");
+        assertThat(calls).hasValue(1);
+    }
+
+    @Test
+    void rawQueryIsPreferred() {
+        // netty-http sets the decoded query in CamelHttpQuery and the raw 
query in CamelHttpRawQuery
+        Exchange exchange = verificationRequest(Exchange.HTTP_QUERY,
+                "hub.mode=subscribe&hub.verify_token=my 
token&more&hub.challenge=100%");
+        exchange.getIn().setHeader(Exchange.HTTP_RAW_QUERY,
+                
"hub.mode=subscribe&hub.verify_token=my%20token%26more&hub.challenge=100%25");
+
+        process(exchange);
+
+        
assertThat(exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isNull();
+        assertThat(exchange.getMessage().getBody()).isEqualTo("100%");
+        assertThat(calls).hasValue(1);
+    }
+
+    @Test
+    void wrongTokenIsRejected() {
+        Exchange exchange = verificationRequest(Exchange.HTTP_QUERY,
+                
"hub.mode=subscribe&hub.verify_token=my%20token&hub.challenge=123");
+
+        process(exchange);
+
+        
assertThat(exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isEqualTo(403);
+        assertThat(exchange.getMessage().getBody()).isNull();
+    }
+
+    @Test
+    void invalidEncodingIsBadRequest() {
+        Exchange exchange = verificationRequest(Exchange.HTTP_QUERY,
+                "hub.mode=subscribe&hub.verify_token=%zz&hub.challenge=123");
+
+        process(exchange);
+
+        
assertThat(exchange.getMessage().getHeader(Exchange.HTTP_RESPONSE_CODE)).isEqualTo(400);
+        assertThat(exchange.getException()).isNull();
+        assertThat(calls).hasValue(0);
+    }
+
+    @Test
+    void missingHttpMethodIsAnEvent() {
+        Exchange exchange = new DefaultExchange(context);
+        exchange.getIn().setBody("{\"object\":\"whatsapp_business_account\"}");
+
+        process(exchange);
+
+        assertThat(exchange.getException()).isNull();
+        
assertThat(exchange.getMessage().getBody()).isEqualTo("{\"object\":\"whatsapp_business_account\"}");
+        assertThat(calls).hasValue(1);
+    }
+
+    @Test
+    void eventWithoutBody() {
+        Exchange exchange = new DefaultExchange(context);
+        exchange.getIn().setHeader(Exchange.HTTP_METHOD, "POST");
+
+        process(exchange);
+
+        assertThat(exchange.getException()).isNull();
+        assertThat(exchange.getMessage().getBody()).isEqualTo("");
+        assertThat(calls).hasValue(1);
+    }
+
+    private Exchange verificationRequest(String header, String query) {
+        Exchange exchange = new DefaultExchange(context);
+        exchange.getIn().setHeader(Exchange.HTTP_METHOD, "GET");
+        exchange.getIn().setHeader(header, query);
+        return exchange;
+    }
+
+    private void process(Exchange exchange) {
+        processor.process(exchange, doneSync -> {
+        });
+    }
+}
diff --git 
a/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookTest.java
 
b/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookTest.java
index 7b34a56d8b36..564df7cb7668 100644
--- 
a/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookTest.java
+++ 
b/components/camel-whatsapp/src/test/java/org/apache/camel/component/whatsapp/WhatsAppWebhookTest.java
@@ -17,6 +17,7 @@
 package org.apache.camel.component.whatsapp;
 
 import java.io.InputStream;
+import java.util.Map;
 
 import org.apache.camel.Exchange;
 import org.apache.camel.RoutesBuilder;
@@ -50,6 +51,21 @@ public class WhatsAppWebhookTest extends WhatsAppTestSupport 
{
         mock.assertIsSatisfied();
     }
 
+    @Test
+    public void testWebhookRegistrationEncodedQuery() throws Exception {
+        WebhookConfiguration config
+                = ((WebhookEndpoint) 
context().getRoute("webhook").getConsumer().getEndpoint()).getConfiguration();
+        // a parameter with an empty value, and an encoded challenge
+        String query = "empty=&hub.mode=subscribe&hub.verify_token=" + 
VERIFY_TOKEN + "&hub.challenge=a%26b";
+
+        MockEndpoint mock = getMockEndpoint("mock:endpoint");
+        mock.expectedBodiesReceived("a&b");
+
+        template().sendBodyAndHeaders("netty-http:" + 
config.computeFullExternalUrl(), null,
+                Map.of(Exchange.HTTP_METHOD, "GET", Exchange.HTTP_QUERY, 
query));
+        mock.assertIsSatisfied();
+    }
+
     @Test
     public void testWebhookRegistrationFailed() throws Exception {
         String challenge = "987";

Reply via email to