Saltar al contenido principal

Seguridad

Dos capas protegen la comunicación: la firma HMAC (prueba que el payload vino del Dunning y no fue adulterado) y la autenticación del request (credenciales que el Dunning presenta a tu endpoint). La primera va siempre; la segunda es opcional.

Firma HMAC

Toda entrega lleva el header:

X-Webhook-Signature: t=<unix>,v1=<hex>
  • t es el timestamp (unix, segundos) del momento del envío — cada reintento se re-firma con un t nuevo.
  • v1 es el HMAC-SHA256 de "{t}.{body}" (timestamp, un punto y el body crudo de la solicitud), calculado con el secret del endpoint (aquel whsec_... mostrado una única vez en el registro), en hexadecimal en minúsculas.

Vincular el timestamp a la firma cierra el replay: un atacante que capture una entrega no puede volver a presentarla más tarde, porque el t queda fuera de la ventana de tolerancia — y no puede cambiar el t sin invalidar el HMAC. Tolerancia recomendada: 5 minutos.

Verificación de la firma

Extrae t y v1, rechaza timestamps fuera de la tolerancia y compara el HMAC de "{t}.{body}" en tiempo constante (siempre sobre el body crudo, antes de cualquier parseo de JSON). El algoritmo es el mismo en cualquier lenguaje — el mismo verificador en Node.js, Python, PHP y Ruby:

const crypto = require('crypto');

const TOLERANCE_SECONDS = 300; // 5 minutos

function verifySignature(rawBody, signatureHeader, secret) {
const t = /(?:^|,)t=(\d+)(?:,|$)/.exec(signatureHeader || '')?.[1];
const v1 = /(?:^|,)v1=([0-9a-f]+)(?:,|$)/i.exec(signatureHeader || '')?.[1];
if (!t || !v1) return false;

// Anti-replay: rechaza firmas fuera de la ventana de tolerancia
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - Number(t)) > TOLERANCE_SECONDS) return false;

const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
const a = Buffer.from(expected, 'hex');
const b = Buffer.from(v1.toLowerCase(), 'hex');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}

// Ejemplo con Express (nota el bodyParser crudo):
app.post('/webhooks/dunning', express.raw({ type: 'application/json' }), (req, res) => {
const ok = verifySignature(
req.body, // Buffer con el body crudo
req.get('X-Webhook-Signature'),
process.env.DUNNING_WEBHOOK_SECRET
);
if (!ok) return res.status(401).end();

const event = JSON.parse(req.body);
// encola y responde rápido
res.status(200).end();
});

Trampas clásicas:

  • No re-serialices el JSON para calcular el HMAC: cualquier diferencia de espacios u orden de claves cambia la firma. Usa el body exactamente como llegó.
  • Concatena en el orden correcto: el HMAC cubre t, un punto (.) y el body — en ese orden.
  • Usa comparación en tiempo constante (crypto.timingSafeEqual), nunca ===: la comparación simple filtra información por timing.
  • Un reloj desincronizado en tu servidor tumba verificaciones legítimas: mantén NTP al día antes de reducir la tolerancia.
  • Rechaza solicitudes sin el header o con firma inválida con 401, sin detallar el motivo.

Un reintento de la misma entrega llega con t (y firma) diferentes — la ventana de tolerancia aplica a cualquier intento. Para deduplicar entregas, usa el header X-Webhook-Delivery-Id, no la firma.

Autenticación del request

Si tu endpoint está detrás de un gateway que exige credenciales, configura la autenticación en el registro:

TipoQué envía el Dunning
Ninguna (none)Solo la firma HMAC
Basic (basic)Authorization: Basic <usuário:senha>
Bearer (bearer)Authorization: Bearer <token>
Header (header)Un header custom con nombre y valor definidos por ti

Las credenciales se cifran en reposo (AES-256-GCM) y nunca aparecen en la pista de entregas: en la captura de request/response, la firma y los headers de autenticación se graban como [REDACTED].

Rotación del secret

El botón de rotación (en la fila del endpoint) genera un secret nuevo, mostrado una única vez, e invalida el anterior de inmediato (también disponible vía POST /api/v1/webhook-endpoints/{id}/rotate-secret). Rota el secret si sospechas de una filtración o como higiene periódica; actualiza el consumidor en ese mismo momento, porque las entregas firmadas con el secret anterior dejarán de validar.

Protecciones del lado del Dunning

  • Solo HTTPS: las URLs http:// se rechazan en el registro.
  • Anti-SSRF: la URL se valida en el registro y se revalida en cada entrega (DNS re-resuelto, IP fijada, redes internas bloqueadas). Los redirects no se siguen. Un destino que pasa a resolver hacia una red interna se bloquea permanentemente (SSRF blocked, sin reintento).
  • Las respuestas de tu endpoint se capturan con los headers sensibles (Set-Cookie, Authorization...) redactados.