Webhook-Überprüfung

Wie Sie überprüfen, dass Webhooks authentisch sind und nicht manipuliert wurden.

Warum überprüfen

Jeder, der die URL Ihres Webhooks kennt, könnte versuchen, gefälschte Ereignisse zu senden. Um dies zu verhindern, enthält jeder Webhook eine kryptografische Signatur, die Sie überprüfen müssen.

Überprüfen Sie immer die Signaturen

Verarbeiten Sie niemals einen Webhook, ohne seine Signatur zu überprüfen, besonders wenn er wichtige Aktionen wie das Erstellen von Bestellungen oder das Ändern von Daten ausführt.

Wie es funktioniert

  1. Beim Erstellen eines Webhooks wird ein secret eindeutiges
  2. Wenn wir einen Webhook senden, berechnen wir einen HMAC-SHA256-Hash des Payloads mit Ihrem Secret
  3. Wir fügen diesen Hash im Header ein X-SalonBookIt-Signature
  4. Ihr Server berechnet den gleichen Hash und vergleicht ihn

Signaturformat

X-SalonBookIt-Signature: sha256=abc123def456...

Die Signatur besteht aus:

  • sha256= - Präfix, das den Algorithmus angibt
  • Gefolgt vom HMAC-SHA256-Hash in hexadezimal

Ihr Secret erhalten

  1. Gehen Sie zu Konfiguration → Integrationen
  2. Klicken Sie im Webhook-Bereich auf den Webhook, den Sie überprüfen möchten
  3. Kopieren Sie den Wert des Feldes Secret
Speichern Sie das Secret sicher

Verwenden Sie Umgebungsvariablen oder einen Secret-Manager. Fügen Sie es niemals in den Quellcode ein.

Die Signatur überprüfen

Node.js / JavaScript

JavaScript
const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
    // Hash der Signatur extrahieren
    const sigHash = signature.replace('sha256=', '');

    // Erwarteten Hash berechnen
    const expectedHash = crypto
        .createHmac('sha256', secret)
        .update(payload, 'utf8')
        .digest('hex');

    // Sicher vergleichen (vermeidet Timing-Angriffe)
    return crypto.timingSafeEqual(
        Buffer.from(sigHash, 'hex'),
        Buffer.from(expectedHash, 'hex')
    );
}

// In deinem Endpunkt
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
    const signature = req.headers['x-salonbookit-signature'];
    const payload = req.body.toString();

    if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
        console.error('Ungültige Signatur');
        return res.status(401).send('Invalid signature');
    }

    const event = JSON.parse(payload);
    console.log('Gültiges Ereignis:', event.type);

    // Ereignis verarbeiten...

    res.json({ received: true });
});

Python

Python
import hmac
import hashlib
import os
from flask import Flask, request, abort

app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET')

def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
    """Überprüft die HMAC-Signatur des Webhooks."""
    if not signature.startswith('sha256='):
        return False

    sig_hash = signature[7:]  # 'sha256=' entfernen

    expected_hash = hmac.new(
        secret.encode('utf-8'),
        payload,
        hashlib.sha256
    ).hexdigest()

    # Sicherer Vergleich gegen Timing-Angriffe
    return hmac.compare_digest(sig_hash, expected_hash)


@app.route('/webhook', methods=['POST'])
def webhook():
    signature = request.headers.get('X-SalonBookIt-Signature', '')
    payload = request.get_data()

    if not verify_signature(payload, signature, WEBHOOK_SECRET):
        abort(401, 'Invalid signature')

    event = request.get_json()
    print(f"Gültiges Ereignis: {event['type']}")

    # Ereignis verarbeiten...

    return {'received': True}

PHP

PHP
<?php

$webhookSecret = getenv('WEBHOOK_SECRET');
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_SALONBOOKIT_SIGNATURE'] ?? '';

function verifySignature(string $payload, string $signature, string $secret): bool
{
    if (strpos($signature, 'sha256=') !== 0) {
        return false;
    }

    $sigHash = substr($signature, 7);
    $expectedHash = hash_hmac('sha256', $payload, $secret);

    return hash_equals($expectedHash, $sigHash);
}

if (!verifySignature($payload, $signature, $webhookSecret)) {
    http_response_code(401);
    die('Invalid signature');
}

$event = json_decode($payload, true);
error_log('Gültiges Ereignis: ' . $event['type']);

// Ereignis verarbeiten...

http_response_code(200);
echo json_encode(['received' => true]);

Ruby

Ruby
require 'openssl'
require 'sinatra'

WEBHOOK_SECRET = ENV['WEBHOOK_SECRET']

def verify_signature(payload, signature, secret)
  return false unless signature.start_with?('sha256=')

  sig_hash = signature[7..]
  expected_hash = OpenSSL::HMAC.hexdigest('sha256', secret, payload)

  Rack::Utils.secure_compare(expected_hash, sig_hash)
end

post '/webhook' do
  payload = request.body.read
  signature = request.env['HTTP_X_SALONBOOKIT_SIGNATURE'] || ''

  unless verify_signature(payload, signature, WEBHOOK_SECRET)
    halt 401, 'Invalid signature'
  end

  event = JSON.parse(payload)
  puts "Gültiges Ereignis: #{event['type']}"

  # Ereignis verarbeiten...

  content_type :json
  { received: true }.to_json
end

Timestamp überprüfen (optional)

Für mehr Sicherheit können Sie überprüfen, dass der Webhook nicht zu alt ist, indem Sie den Header verwenden X-SalonBookIt-Timestamp:

JavaScript
const timestamp = parseInt(req.headers['x-salonbookit-timestamp']);
const now = Math.floor(Date.now() / 1000);
const tolerance = 300; // 5 Minuten

if (Math.abs(now - timestamp) > tolerance) {
    console.error('Webhook zu alt');
    return res.status(401).send('Timestamp expired');
}

Dies verhindert Replay-Angriffe, bei denen jemand versucht, einen zuvor erfassten Webhook erneut zu senden.

Häufige Fehler

Signatur stimmt nicht überein

  • Überprüfe, dass du das richtige Secret verwendest
  • Stelle sicher, dass du den Payload verwendest raw (nicht geparst)
  • Ändere den Payload nicht vor der Überprüfung

Der Payload wurde geändert

Wenn dein Framework das JSON automatisch parst, hole den rohen Body vorher. In Express:

app.use(express.raw({ type: 'application/json' }))

Encoding-Probleme

Stelle sicher, dass der Payload bei der Hash-Berechnung als UTF-8 behandelt wird.