Vérifier les signatures de webhook
Updated February 28, 2026
Voici comment vérifier les signatures de webhook Formspree dans votre code de réception.
Le point clé est que Formspree ne signe pas uniquement le corps brut de la requête. Il signe à la fois l’horodatage et le corps brut, dans ce format :
{timestamp}.{raw_body}
La signature est transmise dans l’en-tête Formspree-Signature selon ce format :
t=<unix>,v1=<hex>
Exigences principales
- Utilisez le corps brut de la requête exactement tel que reçu.
- Analysez l’en-tête
Formspree-Signaturepour extrairetetv1. - Calculez
HMAC_SHA256(secret, f"{t}.{raw_body}")et comparez-le àv1. - Optionnel : rejetez les requêtes plus anciennes qu’une fenêtre de tolérance (protection contre les attaques par rejeu).
Exemples de code
Voici quelques extraits de code pour vous aider à démarrer.
Python (Flask)
import hmac
import hashlib
import time
from flask import Flask, request, abort
app = Flask(__name__)
SIGNING_SECRET = "your_signing_secret" # Set this to the secret shown in Formspree
def verify_formspree_signature(secret, signature_header, raw_body, tolerance=300):
if not signature_header:
return False
try:
# Header format: "t=<unix>,v1=<hex>"
parts = dict(item.split("=", 1) for item in signature_header.split(","))
timestamp = int(parts.get("t", "0"))
received_sig = parts.get("v1")
except Exception:
return False
if not received_sig:
return False
# Optional: reject old requests to prevent replay attacks
if abs(int(time.time()) - timestamp) > tolerance:
return False
# Formspree signs: "{timestamp}.{raw_body}"
signed_payload = f"{timestamp}.{raw_body}"
# Compute HMAC-SHA256 with the shared secret
expected_sig = hmac.new(
key=secret.encode("utf-8"),
msg=signed_payload.encode("utf-8"),
digestmod=hashlib.sha256,
).hexdigest()
# Constant-time compare to avoid timing attacks
return hmac.compare_digest(expected_sig, received_sig)
@app.post("/webhook")
def webhook():
# Important: read the raw request body as sent
raw_body = request.get_data(as_text=True)
signature = request.headers.get("Formspree-Signature", "")
if not verify_formspree_signature(SIGNING_SECRET, signature, raw_body):
abort(401)
return ("ok", 200)
JavaScript (Node.js + Express)
const crypto = require("crypto");
const express = require("express");
const app = express();
const SIGNING_SECRET = "your_signing_secret"; // Set this to the secret shown in Formspree
// Capture the raw body before JSON parsing changes it
app.use(express.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString("utf8");
}
}));
function verifyFormspreeSignature(secret, signatureHeader, rawBody, tolerance = 300) {
if (!signatureHeader) return false;
// Header format: "t=<unix>,v1=<hex>"
const parts = Object.fromEntries(
signatureHeader.split(",").map(s => s.split("=", 2))
);
const timestamp = parseInt(parts.t || "0", 10);
const receivedSig = parts.v1;
if (!receivedSig) return false;
// Optional: reject old requests to prevent replay attacks
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - timestamp) > tolerance) return false;
// Formspree signs: "{timestamp}.{raw_body}"
const signedPayload = `${timestamp}.${rawBody}`;
// Compute HMAC-SHA256 with the shared secret
const expectedSig = crypto
.createHmac("sha256", secret)
.update(signedPayload, "utf8")
.digest("hex");
// Constant-time compare to avoid timing attacks
return crypto.timingSafeEqual(
Buffer.from(expectedSig, "utf8"),
Buffer.from(receivedSig, "utf8")
);
}
app.post("/webhook", (req, res) => {
const signature = req.header("Formspree-Signature") || "";
if (!verifyFormspreeSignature(SIGNING_SECRET, signature, req.rawBody)) {
return res.status(401).send("invalid signature");
}
res.status(200).send("ok");
});
app.listen(3000);
Résolution des problèmes et erreurs courantes
Voici quelques erreurs courantes à éviter :
- Re-sérialiser le JSON avant la signature : vérifiez à partir des octets bruts du corps de la requête exactement tels que reçus, car l’analyse puis la re-sérialisation du JSON modifient la séquence d’octets et invalident le HMAC.
- Signer uniquement le corps (préfixe
t.manquant) : comme mentionné ci-dessus, Formspree signe l’horodatage et le contenu ensemble ; vous devez donc calculer le HMAC surt.<raw_body>(en incluant le délimiteurt.) et non sur le corps seul. - Comparer à la chaîne d’en-tête complète au lieu de la valeur
v1: extrayez la valeur de signaturev1de l’en-tête et comparez uniquement ce condensé hexadécimal, pas le contenu complet de l’en-tête séparé par des virgules. - Utiliser un secret différent de celui configuré pour le webhook : assurez-vous que le secret utilisé pour le HMAC est exactement le secret de signature du webhook dans les paramètres Formspree, et non votre clé API ou un autre secret de projet.