Jede Webhook-Anfrage von NueForm enthält eine kryptografische Signatur, mit der du prüfen kannst, ob die Anfrage echt ist und nicht manipuliert wurde. Du solltest diese Signatur immer verifizieren, bevor du Webhook-Daten verarbeitest.
So funktioniert die Signierung
Wenn NueForm einen Webhook versendet, passiert Folgendes:
- Die Payload wird als JSON-String serialisiert.
- Aus diesem JSON-String wird ein HMAC-SHA256-Hash berechnet, mit deinem Webhook-Secret als Schlüssel.
- Der Hash wird als Hexadezimal-String in Kleinbuchstaben codiert.
- Der Hex-Digest wird im HTTP-Header
X-NueForm-Signaturemitgesendet.
Auf deiner Seite führst du dieselbe Berechnung auf dem rohen Anfrage-Body durch und vergleichst dein Ergebnis mit dem Header-Wert. Stimmen sie überein, ist die Anfrage echt.
Der Signatur-Header
X-NueForm-Signature: 5d41402abc4b2a76b9719d911017c592a3f6e7d4b9c1d5e8f2a7b3c6d9e0f1a2
Der Header-Wert ist ein 64-stelliger Hexadezimal-String (die Ausgabe von HMAC-SHA256).
Dein Webhook-Secret
Dein Webhook-Secret ist ein 64-stelliger Hexadezimal-String, erzeugt aus 32 zufälligen Bytes. Es ist einzigartig für dein Konto und gilt für alle deine Webhook-Endpunkte (formularbezogen und global).
Secret abrufen
Rufe dein aktuelles Secret über die API ab:
curl https://app.nueform.com/api/v1/webhooks/secret \
-H "Authorization: Bearer nf_your_api_key"
Antwort:
{
"secret": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
}
Wenn du noch kein Secret hast, generiert NueForm bei der ersten Anfrage automatisch eines.
Secret neu generieren
Wenn dein Secret kompromittiert wurde, generiere es sofort neu:
curl -X POST https://app.nueform.com/api/v1/webhooks/secret/regenerate \
-H "Authorization: Bearer nf_your_api_key"
Das Neugenerieren macht das alte Secret sofort ungültig. Alle Webhook-Zustellungen ab diesem Zeitpunkt verwenden das neue Secret. Aktualisiere deinen Verifizierungscode vor oder unmittelbar nach dem Neugenerieren, damit du keine gültigen Webhooks ablehnst.
Codebeispiele zur Verifizierung
Node.js
import crypto from 'crypto';
function verifyWebhookSignature(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
// Use timing-safe comparison to prevent timing attacks
const a = Buffer.from(signature, 'hex');
const b = Buffer.from(expected, 'hex');
if (a.length !== b.length) {
return false;
}
return crypto.timingSafeEqual(a, b);
}
// Express.js example
app.post('/webhooks/nueform', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-nueform-signature'];
const rawBody = req.body.toString();
if (!verifyWebhookSignature(rawBody, signature, process.env.NUEFORM_WEBHOOK_SECRET)) {
console.error('Invalid webhook signature');
return res.status(401).send('Invalid signature');
}
const payload = JSON.parse(rawBody);
console.log('Verified webhook:', payload.event, payload.responseId);
// Process the webhook asynchronously
processWebhookAsync(payload);
res.status(200).send('OK');
});
Mit Express.js musst du express.raw() oder express.text() verwenden, um an den rohen Anfrage-Body zu kommen. Nutzt du express.json(), wird der Body geparst und neu serialisiert — dabei kann ein anderer String entstehen als der signierte, und die Verifizierung schlägt fehl.
Python
import hashlib
import hmac
def verify_webhook_signature(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode('utf-8'),
raw_body,
hashlib.sha256
).hexdigest()
# Use timing-safe comparison
return hmac.compare_digest(expected, signature)
# Flask example
from flask import Flask, request, abort
app = Flask(__name__)
@app.route('/webhooks/nueform', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-NueForm-Signature', '')
raw_body = request.get_data()
if not verify_webhook_signature(raw_body, signature, WEBHOOK_SECRET):
abort(401, 'Invalid signature')
payload = request.get_json()
print(f"Verified webhook: {payload['event']} {payload['responseId']}")
# Process asynchronously (e.g., enqueue to Celery)
process_webhook.delay(payload)
return 'OK', 200
Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
)
func verifyWebhookSignature(body []byte, signature string, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
// hmac.Equal performs a constant-time comparison
return hmac.Equal([]byte(expected), []byte(signature))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", http.StatusBadRequest)
return
}
defer r.Body.Close()
signature := r.Header.Get("X-NueForm-Signature")
if !verifyWebhookSignature(body, signature, webhookSecret) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process webhook payload
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
PHP
<?php
function verifyWebhookSignature(string $rawBody, string $signature, string $secret): bool {
$expected = hash_hmac('sha256', $rawBody, $secret);
// Use timing-safe comparison
return hash_equals($expected, $signature);
}
// Usage
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_NUEFORM_SIGNATURE'] ?? '';
$secret = getenv('NUEFORM_WEBHOOK_SECRET');
if (!verifyWebhookSignature($rawBody, $signature, $secret)) {
http_response_code(401);
echo 'Invalid signature';
exit;
}
$payload = json_decode($rawBody, true);
error_log("Verified webhook: {$payload['event']} {$payload['responseId']}");
// Process the webhook
processWebhook($payload);
http_response_code(200);
echo 'OK';
Ruby
require 'openssl'
require 'json'
def verify_webhook_signature(raw_body, signature, secret)
expected = OpenSSL::HMAC.hexdigest('SHA256', secret, raw_body)
# Use timing-safe comparison
Rack::Utils.secure_compare(expected, signature)
end
# Sinatra example
post '/webhooks/nueform' do
raw_body = request.body.read
signature = request.env['HTTP_X_NUEFORM_SIGNATURE'] || ''
unless verify_webhook_signature(raw_body, signature, ENV['NUEFORM_WEBHOOK_SECRET'])
halt 401, 'Invalid signature'
end
payload = JSON.parse(raw_body)
logger.info "Verified webhook: #{payload['event']} #{payload['responseId']}"
# Process asynchronously
WebhookProcessorJob.perform_async(payload)
status 200
body 'OK'
end
Zeitkonstanter Vergleich
Alle obigen Codebeispiele verwenden beim Prüfen der Signaturen einen zeitkonstanten Vergleich (auch Constant-Time-Vergleich genannt). Das ist eine Sicherheits-Best-Practice, die Timing-Angriffe verhindert.
Ein Timing-Angriff misst, wie lange ein String-Vergleich dauert. Ein naiver ==-Vergleich gibt false zurück, sobald das erste abweichende Zeichen gefunden wird — ein Angreifer könnte so durch Messen der Antwortlatenz die Signatur Zeichen für Zeichen erraten.
Zeitkonstante Vergleichsfunktionen benötigen immer gleich viel Zeit, egal wie viele Zeichen übereinstimmen, und machen diesen Angriff damit praktisch unmöglich.
| Sprache | Funktion |
|---|---|
| Node.js | crypto.timingSafeEqual() |
| Python | hmac.compare_digest() |
| Go | hmac.Equal() |
| PHP | hash_equals() |
| Ruby | Rack::Utils.secure_compare() |
Verwende niemals ===, == oder .equals(), um HMAC-Signaturen zu vergleichen. Nutze immer die eingebaute zeitkonstante Vergleichsfunktion deiner Sprache.
Was tun, wenn die Verifizierung fehlschlägt
Wenn die Signaturprüfung fehlschlägt, sollte dein Endpunkt:
- Den Statuscode
401 Unauthorizedzurückgeben. Verarbeite die Payload nicht. - Den Fehlschlag protokollieren — fürs Debugging. Nimm Anfrage-IP, Zeitstempel und (optional) die empfangene Signatur auf.
- Dein Secret niemals offenlegen — weder in Fehlermeldungen noch in Logs.
- Häufige Ursachen untersuchen:
- Falsches Secret. Stelle sicher, dass du das aktuelle Webhook-Secret verwendest. Wenn du es kürzlich neu generiert hast, aktualisiere deinen Verifizierungscode.
- Veränderter Body. Verifiziere gegen den rohen Anfrage-Body, nicht gegen eine geparste und neu serialisierte Version. Middleware, die JSON vor deinem Verifizierungscode parst, ist die häufigste Fehlerursache.
- Encoding-Probleme. Der rohe Body muss als UTF-8-Bytes behandelt werden. Stelle sicher, dass dein Framework keine unerwarteten Encoding-Transformationen anwendet.
- Veränderung durch Proxys. Wenn ein Reverse Proxy (z. B. Cloudflare, nginx) den Anfrage-Body verändert, stimmt die Signatur nicht. Konfiguriere deinen Proxy so, dass er den Body unverändert durchreicht.
Checkliste zur Fehlersuche
| Problem | Lösung |
|---|---|
| Signatur stimmt nie überein | Prüfe, ob du die rohen Body-Bytes liest und nicht ein geparstes JSON-Objekt |
| Signatur stimmt plötzlich nicht mehr | Prüfe, ob dein Webhook-Secret neu generiert wurde |
| Signatur stimmt lokal, aber nicht in der Produktion | Prüfe auf Body-Veränderungen durch Proxy oder CDN |
X-NueForm-Signature-Header fehlt | Stelle sicher, dass dein Framework benutzerdefinierte Header erhält (manche entfernen Header mit X--Präfix) |
Nächste Schritte
- Testen --- Signaturprüfung lokal testen
- Payloads --- Das Payload-Format verstehen, das du verifizierst
- Ereignisse --- Die Ereignistypen kennenlernen