POST /api/rebuild-hook : un endpoint aussi simple que celui-ci, sans aucune vérification, peut être appelé par n’importe qui capable de deviner son URL. Sur un site vitrine alimenté par des campagnes d’emailing Brevo dont le contenu déclenchait un rebuild statique Vercel à chaque mise à jour d’un modèle de newsletter, cette porte ouverte n’a été identifiée qu’après un audit de sécurité de routine, avant qu’elle ne soit exploitée.
Un attaquant capable d’appeler cet endpoint à répétition aurait pu épuiser en quelques minutes le quota de builds mensuel de l’hébergeur, provoquant un déni de service indirect sans même avoir besoin de compromettre quoi que ce soit côté WordPress ou Brevo. La correction a consisté à vérifier systématiquement la signature du webhook avant de déclencher le moindre rebuild.
Le problème : un endpoint public sans validation
L’endpoint de rebuild, une simple fonction serverless Vercel, se contentait de recevoir une requête POST et de relayer un appel vers l’API de déploiement Vercel dès réception, sans se soucier de vérifier que l’appelant était réellement Brevo :
// Version initiale, non sécurisée
export default async function handler(req, res) {
await fetch(process.env.VERCEL_DEPLOY_HOOK_URL, { method: 'POST' });
res.status(200).json({ ok: true });
}
N’importe quelle requête POST vers cette URL, connue ou devinée, déclenchait un rebuild complet du site, indépendamment de sa provenance réelle.
Vérifier la signature avant tout traitement
Brevo transmet, selon la configuration du webhook, un en-tête personnalisé permettant de vérifier l’authenticité de l’appel. La pratique retenue consiste à définir, côté configuration du webhook Brevo, un jeton secret partagé, transmis dans un en-tête personnalisé, puis à le comparer côté serveur avant tout traitement :

export default async function handler(req, res) {
const secretRecu = req.headers['x-brevo-secret'];
if (secretRecu !== process.env.BREVO_WEBHOOK_SECRET) {
return res.status(401).json({ erreur: 'signature invalide' });
}
await fetch(process.env.VERCEL_DEPLOY_HOOK_URL, { method: 'POST' });
res.status(200).json({ ok: true });
}
Cette vérification, volontairement simple, repose sur une comparaison directe de chaîne. Pour un usage plus exigeant, une comparaison à temps constant (via crypto.timingSafeEqual en Node.js) évite en plus les attaques par mesure de temps de réponse, bien que le risque reste marginal pour ce cas d’usage précis.
Limiter la fréquence des rebuilds
Même avec une signature valide, un événement Brevo légitime mais répété rapidement (plusieurs campagnes modifiées coup sur coup) peut déclencher une rafale de rebuilds inutiles. Une limitation de fréquence, stockée dans une base clé-valeur légère, empêche plus d’un rebuild toutes les cinq minutes :
const dernierRebuild = await kv.get('dernier_rebuild_brevo');
const maintenant = Date.now();
if (dernierRebuild && maintenant - dernierRebuild < 5 * 60 * 1000) {
return res.status(429).json({ erreur: 'rebuild déjà déclenché récemment' });
}
await kv.set('dernier_rebuild_brevo', maintenant);
Journaliser chaque déclenchement
Chaque appel réussi à l’endpoint est désormais journalisé avec son horodatage et un identifiant d’événement Brevo si disponible, permettant de retracer a posteriori l’origine de chaque rebuild déclenché en cas de doute ou d’incident de production.
- Horodatage de la requête reçue
- Résultat de la vérification de signature (accepté ou rejeté)
- Identifiant du déploiement Vercel déclenché en retour
Notre verdict
Un endpoint de déclenchement de rebuild sans vérification d’origine constitue une vulnérabilité aussi simple à exploiter qu’à corriger. La vérification de signature, associée à une limitation de fréquence, transforme un point d’entrée ouvert en un mécanisme fiable, sans jamais avoir eu besoin de toucher à la configuration des campagnes d’emailing elles-mêmes, qui reste un sujet totalement distinct.