Skip to main content

Overview

Securing your webhook endpoints is critical to prevent:
  • Unauthorized access - Fake requests pretending to be from WhizoAI
  • Replay attacks - Attackers re-sending captured webhook payloads
  • Man-in-the-middle attacks - Intercepted and modified webhooks
  • Data tampering - Modified payload data

Signature Verification

WhizoAI signs all webhook requests with HMAC-SHA256. Always verify the signature before processing events.

How Signatures Work

  1. WhizoAI creates an HMAC signature using your webhook secret
  2. Signature is sent in the X-WhizoAI-Signature header
  3. Your server recalculates the signature using the same secret
  4. Compare signatures - if they match, the request is authentic

Implementation

import hmac
import hashlib
from flask import Flask, request, jsonify

app = Flask(__name__)
WEBHOOK_SECRET = "your_webhook_secret"

@app.route('/webhook', methods=['POST'])
def webhook():
    # Get signature from header
    signature = request.headers.get('X-WhizoAI-Signature')
    if not signature:
        return jsonify({"error": "No signature provided"}), 401

    # Get raw payload (important: don't parse as JSON first!)
    payload = request.get_data()

    # Verify signature
    if not verify_signature(payload, signature, WEBHOOK_SECRET):
        return jsonify({"error": "Invalid signature"}), 401

    # Now safe to process event
    event = request.json
    process_event(event)

    return jsonify({"received": True}), 200

def verify_signature(payload, signature, secret):
    """
    Verify webhook signature using HMAC-SHA256
    """
    expected_signature = hmac.new(
        secret.encode('utf-8'),
        payload,
        hashlib.sha256
    ).hexdigest()

    # Use timing-safe comparison to prevent timing attacks
    return hmac.compare_digest(signature, expected_signature)

Common Security Mistakes

❌ Don’t do this:
# WRONG: Parsing JSON before signature verification
event = request.json  # Don't do this first!
signature = request.headers.get('X-WhizoAI-Signature')
if verify_signature(json.dumps(event), signature, secret):
    # This is vulnerable!
✅ Do this instead:
# CORRECT: Verify signature on raw payload
payload = request.get_data()
signature = request.headers.get('X-WhizoAI-Signature')
if verify_signature(payload, signature, secret):
    event = request.json  # Now safe to parse

Additional Security Measures

1. HTTPS Only

Always use HTTPS for webhook URLs. WhizoAI requires HTTPS in production:
# ✅ Good
webhook_url = "https://your-server.com/webhook"

# ❌ Bad - will be rejected
webhook_url = "http://your-server.com/webhook"

2. IP Allowlisting (Optional)

For extra security, allowlist WhizoAI’s IP addresses:
WHIZOAI_IPS = [
    "52.7.43.24",
    "54.165.23.156",
    "34.230.45.67",
    # Add all WhizoAI IPs (contact support for full list)
]

@app.before_request
def check_ip():
    client_ip = request.headers.get('X-Forwarded-For', request.remote_addr)
    if client_ip not in WHIZOAI_IPS:
        return jsonify({"error": "Unauthorized IP"}), 403
Contact [email protected] for the current list of WhizoAI IP addresses.

3. Replay Attack Prevention

Prevent replay attacks by checking timestamps:
from datetime import datetime, timedelta

@app.route('/webhook', methods=['POST'])
def webhook():
    # Verify signature first
    if not verify_signature(...):
        return jsonify({"error": "Invalid signature"}), 401

    event = request.json
    timestamp = datetime.fromisoformat(event['timestamp'])
    now = datetime.utcnow()

    # Reject events older than 5 minutes
    if now - timestamp > timedelta(minutes=5):
        return jsonify({"error": "Event too old"}), 401

    # Process event...
    return jsonify({"received": True}), 200

4. Idempotency

Prevent duplicate processing using event IDs:
import redis

redis_client = redis.Redis()
PROCESSED_EVENTS_KEY = "processed_webhook_events"

def process_event(event):
    event_id = event['id']

    # Check if already processed
    if redis_client.sismember(PROCESSED_EVENTS_KEY, event_id):
        print(f"Event {event_id} already processed, skipping")
        return

    # Process the event
    handle_event(event)

    # Mark as processed (expire after 7 days)
    redis_client.sadd(PROCESSED_EVENTS_KEY, event_id)
    redis_client.expire(PROCESSED_EVENTS_KEY, 604800)

5. Rate Limiting

Implement rate limiting to prevent abuse:
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address

limiter = Limiter(
    app,
    key_func=get_remote_address,
    default_limits=["100 per hour"]
)

@app.route('/webhook', methods=['POST'])
@limiter.limit("200 per minute")  # Allow burst traffic
def webhook():
    # Handle webhook...
    pass

Secret Management

Best Practices

Never hardcode secrets in your code. Use environment variables or secret management services:
import os

# ✅ Good - Use environment variables
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET')

# ❌ Bad - Hardcoded secret
WEBHOOK_SECRET = "my_secret_key_123"  # Don't do this!
Rotate webhook secrets every 90 days:
# Update webhook secret
client.webhooks.update(
    webhook_id="wh_123abc",
    secret="new_secret_key"
)
Support both old and new secrets during rotation period:
def verify_signature(payload, signature, secrets):
    # Try all active secrets (for rotation period)
    for secret in secrets:
        expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
        if hmac.compare_digest(signature, expected):
            return True
    return False

# During rotation, accept both secrets
WEBHOOK_SECRETS = [
    os.environ.get('WEBHOOK_SECRET'),
    os.environ.get('WEBHOOK_SECRET_OLD')  # Remove after rotation complete
]
Use separate secrets for development, staging, and production:
# Development
WEBHOOK_SECRET_DEV = os.environ.get('WEBHOOK_SECRET_DEV')

# Staging
WEBHOOK_SECRET_STAGING = os.environ.get('WEBHOOK_SECRET_STAGING')

# Production
WEBHOOK_SECRET_PROD = os.environ.get('WEBHOOK_SECRET_PROD')

Error Responses

Return appropriate status codes for security errors:
Status CodeWhen to Use
401 UnauthorizedMissing or invalid signature
403 ForbiddenIP not allowlisted
429 Too Many RequestsRate limit exceeded
500 Internal Server ErrorServer-side processing error
@app.route('/webhook', methods=['POST'])
def webhook():
    # No signature provided
    if 'X-WhizoAI-Signature' not in request.headers:
        return jsonify({
            "error": "Unauthorized",
            "message": "No signature provided"
        }), 401

    # Invalid signature
    if not verify_signature(...):
        return jsonify({
            "error": "Unauthorized",
            "message": "Invalid signature"
        }), 401

    # IP not allowed
    if not is_ip_allowed(request.remote_addr):
        return jsonify({
            "error": "Forbidden",
            "message": "IP not allowlisted"
        }), 403

    # Success
    return jsonify({"received": True}), 200

Monitoring & Logging

Log Security Events

import logging

logger = logging.getLogger(__name__)

@app.route('/webhook', methods=['POST'])
def webhook():
    client_ip = request.headers.get('X-Forwarded-For', request.remote_addr)

    if not verify_signature(...):
        logger.warning(
            f"Invalid webhook signature from IP: {client_ip}",
            extra={
                "ip": client_ip,
                "user_agent": request.headers.get('User-Agent'),
                "event_type": request.json.get('event') if request.is_json else None
            }
        )
        return jsonify({"error": "Invalid signature"}), 401

    logger.info(
        f"Valid webhook received from IP: {client_ip}",
        extra={"event": request.json}
    )

    return jsonify({"received": True}), 200

Alert on Suspicious Activity

def check_suspicious_activity():
    # Alert if multiple failed signature verifications
    failed_attempts = redis_client.incr('webhook_failed_signatures')
    redis_client.expire('webhook_failed_signatures', 3600)  # 1 hour window

    if failed_attempts > 10:
        send_security_alert(
            "Multiple failed webhook signature verifications",
            details=f"{failed_attempts} failed attempts in the last hour"
        )

Security Checklist

1

Enable HTTPS

Use HTTPS URLs for all webhook endpoints
2

Verify Signatures

Always verify X-WhizoAI-Signature header before processing
3

Use Raw Payload

Verify signature on raw request body, not parsed JSON
4

Timing-Safe Comparison

Use hmac.compare_digest() or equivalent to prevent timing attacks
5

Check Timestamps

Reject events older than 5 minutes to prevent replay attacks
6

Implement Idempotency

Track processed event IDs to prevent duplicate processing
7

Store Secrets Securely

Use environment variables or secret management services
8

Rate Limiting

Implement rate limiting on webhook endpoints
9

Monitor & Log

Log all webhook requests and alert on suspicious activity
10

Rotate Secrets

Rotate webhook secrets every 90 days

Next Steps