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 <noreply@anthropic.com>
This commit is contained in:
@@ -97,6 +97,9 @@ MAILGUN_SIGNATURE_VALIDATION=true
|
|||||||
SENDBLUE_API_KEY=your-sendblue-api-key
|
SENDBLUE_API_KEY=your-sendblue-api-key
|
||||||
SENDBLUE_API_SECRET=your-sendblue-api-secret
|
SENDBLUE_API_SECRET=your-sendblue-api-secret
|
||||||
SENDBLUE_FROM_NUMBER=+1XXXXXXXXXX
|
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)
|
# Comma-separated list of iMessage numbers allowed to use the service (E.164 format)
|
||||||
# Use * to allow any number
|
# Use * to allow any number
|
||||||
ALLOWED_IMESSAGE_NUMBERS=
|
ALLOWED_IMESSAGE_NUMBERS=
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
|
import hmac
|
||||||
import logging
|
import logging
|
||||||
|
import functools
|
||||||
import time
|
import time
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
@@ -74,7 +76,31 @@ async def send_imessage(to_number: str, content: str) -> dict:
|
|||||||
return resp.json()
|
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"])
|
@imessage_blueprint.route("/webhook", methods=["POST"])
|
||||||
|
@validate_sendblue_signature
|
||||||
async def webhook():
|
async def webhook():
|
||||||
"""Handle incoming iMessages from SendBlue."""
|
"""Handle incoming iMessages from SendBlue."""
|
||||||
data = await request.get_json()
|
data = await request.get_json()
|
||||||
|
|||||||
Reference in New Issue
Block a user