From 9a77144f0eef6aba66e74b4a3eb135d0f68ff000 Mon Sep 17 00:00:00 2001 From: Adriaan Zonnenberg Date: Fri, 6 Feb 2026 16:36:51 +0100 Subject: [PATCH 1/2] Add webhook signature verification --- lib/Modules/OAuth.php | 8 +++-- lib/Modules/Orders/OrdersModule.php | 2 -- lib/Modules/Webhooks.php | 49 +++++++++++++++++++++++++++++ readme.txt | 1 + 4 files changed, 56 insertions(+), 4 deletions(-) diff --git a/lib/Modules/OAuth.php b/lib/Modules/OAuth.php index 8a6f925..88d70e5 100644 --- a/lib/Modules/OAuth.php +++ b/lib/Modules/OAuth.php @@ -2,7 +2,7 @@ namespace Sendy\WooCommerce\Modules; -use GuzzleHttp\Exception\GuzzleException; +use Sendy\Api\Exceptions\SendyException; use Sendy\WooCommerce\ApiClientFactory; class OAuth @@ -52,6 +52,8 @@ public function reset_credentials_when_access_token_nullified($old_value, $value update_option('sendy_refresh_token', null, false); update_option('sendy_token_expires', null, false); + + delete_option('sendy_webhook_secret'); } } @@ -76,10 +78,12 @@ public function oauth_callback(): void 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')); diff --git a/lib/Modules/Orders/OrdersModule.php b/lib/Modules/Orders/OrdersModule.php index ac14d41..66f45c7 100644 --- a/lib/Modules/Orders/OrdersModule.php +++ b/lib/Modules/Orders/OrdersModule.php @@ -136,8 +136,6 @@ protected function create_shipment_with_smart_rules(\WC_Order $order, bool $exec /** * Fetch the labels from the Sendy API and offer them as download to the user - * - * @throws \GuzzleHttp\Exception\GuzzleException */ protected function offer_labels_as_download(array $shipment_ids): void { diff --git a/lib/Modules/Webhooks.php b/lib/Modules/Webhooks.php index e937d3a..4d79e13 100644 --- a/lib/Modules/Webhooks.php +++ b/lib/Modules/Webhooks.php @@ -3,6 +3,7 @@ namespace Sendy\WooCommerce\Modules; use Sendy\Api\ApiException; +use Sendy\Api\Exceptions\SendyException; use Sendy\WooCommerce\ApiClientFactory; use Sendy\WooCommerce\Enums\ProcessingMethod; use WC_Order; @@ -53,6 +54,12 @@ public function init_rest_api_endpoint(): void 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 @@ public function ensure_webhook_installed(): void } } + /** + * @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 @@ public function deactivate(): void 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 */ diff --git a/readme.txt b/readme.txt index ae72096..66c3949 100644 --- a/readme.txt +++ b/readme.txt @@ -54,6 +54,7 @@ Hierop zijn onze [algemene voorwaarden](https://sendy.nl/algemene-voorwaarden/) = Unreleased = * Improve error handling on order pages +* Fix CVE-2025-68564 - Verify webhook requests using the signature = 3.4.1 = * Fix an error handling issue when creating shipments From ef3a6f050eed464d84c25f938dae8fcdb762a201 Mon Sep 17 00:00:00 2001 From: Adriaan Zonnenberg Date: Wed, 11 Feb 2026 11:01:46 +0100 Subject: [PATCH 2/2] 3.4.2 --- lib/Plugin.php | 2 +- readme.txt | 6 +++--- sendy.php | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/Plugin.php b/lib/Plugin.php index bcca6a3..7ef863e 100644 --- a/lib/Plugin.php +++ b/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'; diff --git a/readme.txt b/readme.txt index 66c3949..553675c 100644 --- a/readme.txt +++ b/readme.txt @@ -2,8 +2,8 @@ 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 -Stable tag: 3.4.1 +Version: 3.4.2 +Stable tag: 3.4.2 License: MIT Author: Sendy Author URI: https://sendy.nl/ @@ -52,7 +52,7 @@ Hierop zijn onze [algemene voorwaarden](https://sendy.nl/algemene-voorwaarden/) == Changelog == -= Unreleased = += 3.4.2 = * Improve error handling on order pages * Fix CVE-2025-68564 - Verify webhook requests using the signature diff --git a/sendy.php b/sendy.php index 69dffda..3b8b31c 100644 --- a/sendy.php +++ b/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