From 1e753bfaab439023ce9e95d87bbdaf5cee03bded Mon Sep 17 00:00:00 2001 From: Ryan Chen Date: Wed, 3 Jun 2026 19:28:35 -0400 Subject: [PATCH] Add SendBlue webhook signature validation Validates sb-signing-secret header against SENDBLUE_WEBHOOK_SECRET env var. Can be disabled with SENDBLUE_SIGNATURE_VALIDATION=false for development. Co-Authored-By: Claude Opus 4.6 --- .env.example | 3 +++ blueprints/imessage/__init__.py | 26 ++++++++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/.env.example b/.env.example index e8965fd..88d5567 100644 --- a/.env.example +++ b/.env.example @@ -97,6 +97,9 @@ MAILGUN_SIGNATURE_VALIDATION=true SENDBLUE_API_KEY=your-sendblue-api-key SENDBLUE_API_SECRET=your-sendblue-api-secret SENDBLUE_FROM_NUMBER=+1XXXXXXXXXX +SENDBLUE_WEBHOOK_SECRET=your-sendblue-webhook-secret +# Set to false to disable SendBlue signature validation in development +SENDBLUE_SIGNATURE_VALIDATION=true # Comma-separated list of iMessage numbers allowed to use the service (E.164 format) # Use * to allow any number ALLOWED_IMESSAGE_NUMBERS= diff --git a/blueprints/imessage/__init__.py b/blueprints/imessage/__init__.py index 32a8204..56b9c23 100644 --- a/blueprints/imessage/__init__.py +++ b/blueprints/imessage/__init__.py @@ -1,5 +1,7 @@ import os +import hmac import logging +import functools import time from collections import defaultdict @@ -74,7 +76,31 @@ async def send_imessage(to_number: str, content: str) -> dict: return resp.json() +def validate_sendblue_signature(f): + """Decorator to validate the SendBlue webhook signing secret.""" + + @functools.wraps(f) + async def decorated_function(*args, **kwargs): + if os.getenv("SENDBLUE_SIGNATURE_VALIDATION", "true").lower() == "false": + return await f(*args, **kwargs) + + secret = os.getenv("SENDBLUE_WEBHOOK_SECRET") + if not secret: + logger.error("SENDBLUE_WEBHOOK_SECRET not set — rejecting request") + return jsonify({"error": "Server misconfigured"}), 500 + + sig = request.headers.get("sb-signing-secret", "") + if not hmac.compare_digest(sig, secret): + logger.warning("Invalid SendBlue signing secret") + return jsonify({"error": "Unauthorized"}), 403 + + return await f(*args, **kwargs) + + return decorated_function + + @imessage_blueprint.route("/webhook", methods=["POST"]) +@validate_sendblue_signature async def webhook(): """Handle incoming iMessages from SendBlue.""" data = await request.get_json()