Webhooks confiables y seguros: firmas, idempotencia, reintentos y DLQ
Un webhook parece sencillo: un servicio envía una petición HTTP cuando ocurre algo. El problema aparece cuando la red falla, el proveedor reintenta, llegan dos eventos iguales o alguien intenta falsificar una entrega. Un endpoint fiable debe responder a esas situaciones como parte del diseño, no como una corrección posterior.
- El objetivo real: aceptar una entrega sin ejecutarla dos veces
- Verifica la firma antes de interpretar el JSON
- Evita ataques de repetición
- Responde rápido y procesa fuera del ciclo HTTP
- Diseña para eventos fuera de orden
- Qué debes registrar y qué nunca debes registrar
- Checklist de puesta en producción
- Fuentes y lectura técnica
El objetivo real: aceptar una entrega sin ejecutarla dos veces
La entrega de un webhook suele ser de tipo al menos una vez. Eso significa que el mismo evento puede llegar de nuevo después de un timeout o de una respuesta con error. Por eso el identificador del evento debe tratarse como una clave de negocio. Antes de cambiar un pedido, emitir un reembolso o enviar una notificación, guarda el identificador y decide qué hacer si ya existe.
CREATE TABLE webhook_events (
event_id text PRIMARY KEY,
event_type text NOT NULL,
received_at timestamptz NOT NULL DEFAULT now(),
processed_at timestamptz,
status text NOT NULL DEFAULT 'received'
);
La restricción única es importante: dos procesos concurrentes no deben poder registrar el mismo evento como nuevo. Si el evento ya está guardado, responde de forma segura y evita repetir el efecto externo.
Verifica la firma antes de interpretar el JSON
Una firma HMAC demuestra que el emisor conoce un secreto compartido y que el cuerpo no cambió. No reconstruyas el JSON antes de verificarlo: cambiar espacios, orden de propiedades o saltos de línea puede producir una firma diferente. Lee el cuerpo crudo, valida el esquema de firma esperado y compara el resultado con una función resistente a ataques de tiempo.
const rawBody = await readRawBody(req);
const received = req.header('X-Webhook-Signature');
const expected = createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(rawBody)
.digest('hex');
if (!timingSafeEqual(Buffer.from(received), Buffer.from(expected))) {
return res.sendStatus(401);
}
const event = JSON.parse(rawBody);
El código es una guía de flujo, no una sustitución de la biblioteca del proveedor. Si existe un SDK oficial, úsalo y sigue exactamente su formato. Nunca escribas el secreto en el repositorio, en una imagen de contenedor ni en los logs.
Evita ataques de repetición
Una petición válida puede ser capturada y enviada otra vez. Para reducir ese riesgo, firma también una marca de tiempo y exige una ventana de frescura, por ejemplo cinco minutos, siempre que el proveedor lo permita. El reloj del servidor debe estar sincronizado. La comprobación de tiempo no reemplaza el registro persistente del evento: ambas defensas cumplen funciones distintas.
Durante una rotación planificada, acepta temporalmente el secreto anterior y el nuevo. Cuando todas las entregas verifiquen con el nuevo, revoca el anterior. Si el secreto se sospecha comprometido, no prolongues la ventana: revócalo y revisa los eventos recibidos.
Responde rápido y procesa fuera del ciclo HTTP
El endpoint no debería ejecutar dentro de la petición todas las operaciones de negocio. Primero valida la autenticidad, el tipo y el tamaño del mensaje. Después registra o encola el evento de forma durable. Solo entonces devuelve 2xx. Un worker puede consumir la cola, aplicar la idempotencia y actualizar el sistema principal.
POST /webhooks/provider
1. aceptar solo POST y HTTPS
2. leer cuerpo crudo y verificar firma
3. validar tamaño y esquema
4. insertar event_id con clave única
5. encolar si es nuevo
6. responder 202 o 200
worker
7. tomar evento
8. ejecutar efecto dentro de una transacción
9. marcar processed_at o enviar a DLQ
No devuelvas éxito si el evento desapareció. Una cola de mensajes muertos (DLQ) conserva los eventos que agotaron sus reintentos y permite investigar sin bloquear todos los demás.
Diseña para eventos fuera de orden
Los proveedores no siempre entregan los eventos en el mismo orden en que ocurrieron. Un evento de actualización puede llegar antes que el de creación. No uses la hora de llegada como verdad ni supongas que un contador local representa el orden global. Cuando sea posible, recupera el recurso actual mediante la API del proveedor y usa estados válidos para evitar regresiones.
Qué debes registrar y qué nunca debes registrar
Registra el identificador del evento, su tipo, el proveedor, el estado HTTP, la latencia, el número de intento y el resultado del worker. No registres el cuerpo completo por defecto: puede contener datos personales. Tampoco guardes secretos, tokens completos, cookies o cabeceras de autorización. Configura alertas para aumentos de firmas inválidas, respuestas 5xx, eventos en la DLQ y retrasos de procesamiento.
Checklist de puesta en producción
- HTTPS, método POST y límite de tamaño configurados.
- Firma verificada con el cuerpo crudo y comparación en tiempo constante.
- Marca de tiempo y protección contra replay cuando el proveedor las soporte.
- Identificador persistido con restricción única para garantizar idempotencia.
- Respuesta 2xx solo después de guardar o encolar de forma durable.
- Reintentos con backoff, DLQ y procedimiento de recuperación documentado.
- Secretos fuera del código, rotación probada y logs sin datos sensibles.
Fuentes y lectura técnica
La documentación de Stripe sobre webhooks explica la verificación de firmas, el cuerpo crudo, los duplicados, el orden de eventos y la respuesta rápida. La guía de seguridad de webhooks de OWASP amplía la protección contra replay, la rotación de secretos, la validación de entradas y la observabilidad.
Si quieres conocer otros artículos parecidos a Webhooks confiables y seguros: firmas, idempotencia, reintentos y DLQ puedes visitar la categoría Blog.
Deja una respuesta

También puede gustarte: