Verificar la firma
Cada aviso va firmado con el secreto del webhook. Compruébalo siempre antes de hacer nada con él: si no, cualquiera que conozca tu URL podría mandarte avisos falsos.
La cabecera
http
X-BuildNexion-Signature: sha256=5d1f0c9e8b7a6d5c4b3a2f1e0d9c8b7a6f5e4d3c2b1a0f9e8d7c6b5a4f3e2d1cEs sha256= seguido del HMAC-SHA256, en hexadecimal, del cuerpo de la petición tal cual llega, calculado con el secreto del webhook (el whsec_… que se muestra al crearlo).
Cómo comprobarla
- Lee el cuerpo en crudo, sin convertirlo todavía a JSON.
- Calcula el HMAC-SHA256 de ese cuerpo con tu secreto.
- Compáralo con lo que va después de
sha256=, con una comparación en tiempo constante. - Si no coincide, responde
401y no lo proceses.
No vuelvas a convertir el JSON en texto para comprobar la firma:
JSON.stringify(JSON.parse(cuerpo)) no da los mismos bytes (espacios, orden de las claves…) y la firma no cuadraría.import crypto from 'node:crypto'
// cuerpo: el cuerpo en crudo (Buffer o string), ANTES de hacer JSON.parse
export function firmaValida(cuerpo, cabecera, secreto) {
if (!cabecera || !cabecera.startsWith('sha256=')) return false
const esperada = crypto.createHmac('sha256', secreto).update(cuerpo).digest()
const recibida = Buffer.from(cabecera.slice('sha256='.length), 'hex')
// Comparación en tiempo constante
return recibida.length === esperada.length && crypto.timingSafeEqual(recibida, esperada)
}Un endpoint completo
import express from 'express'
import { firmaValida } from './firma.js'
const app = express()
// express.raw, NO express.json: la firma se comprueba sobre el cuerpo en crudo
app.post('/buildnexion/webhook', express.raw({ type: 'application/json' }), (req, res) => {
if (!firmaValida(req.body, req.get('X-BuildNexion-Signature'), process.env.BUILDNEXION_WEBHOOK_SECRET)) {
return res.status(401).send('firma no válida')
}
const evento = JSON.parse(req.body.toString('utf8'))
res.sendStatus(200) // responde enseguida…
encolar(evento, req.get('X-BuildNexion-Delivery')) // …y procesa después
})Repeticiones
- No proceses dos veces lo mismo: guarda el
X-BuildNexion-Deliveryde lo que ya has procesado. Los reintentos llevan el mismo. - Avisos antiguos: la firma no lleva hora, pero
created_atva dentro del cuerpo firmado y no se puede cambiar. Si quieres descartar avisos viejos, rechaza los de más de una hora (los reintentos llegan como mucho unos 35 minutos después).
Cambiar el secreto
El secreto no se puede cambiar en un webhook ya creado. Si se ha filtrado, crea otro webhook con la misma URL (tendrá un secreto nuevo), actualiza tu servidor y borra el antiguo. Mientras convivan, cada aviso llegará dos veces, una firmada con cada secreto: acepta los dos durante el cambio y descarta el repetido.