HMAC ¿Cómo saben los webhooks que no estás mintiendo?
Cada vez que GitHub, Stripe o Shopify envía un webhook a tu servidor, firmarán el payload con HMAC. Aquí te muestro exactamente cómo funciona y cómo verificarlo en tu propio código.
Recibes una solicitud POST que afirma provenir de Stripe. El payload indica que una transacción ha tenido éxito. Procesas el pedido, envías los productos —y tres días después descubres que la solicitud fue falsa. Ouch.
Esta es la razón por la que cada proveedor de webhooks firma sus payloads usando HMAC. Si entiendes el HMAC, entiendes por qué GitHub, Stripe, Shopify, Twilio y prácticamente cualquier API moderna lo utiliza —y sabrás exactamente cómo verificar esas firmas en tu propio código.
¿Qué es realmente el HMAC?
HMAC significa Código de autenticación de mensaje basado en hash. Responde a una pregunta: «¿Sabía la persona que me envió este mensaje el secreto compartido?»
Funciona generando un hash criptográfico —normalmente SHA-256— sobre la combinación de una clave secreta y el cuerpo del mensaje. El resultado es una cadena de longitud fija que:
- cambia completamente si incluso un byte del mensaje cambia
- no puede ser producido sin conocer la clave secreta
- no puede ser revertido para revelar la clave o el mensaje original
La fórmula es compacta: HMAC(key, message) = H((key ⊕ opad) || H((key ⊕ ipad) || message)). No necesitas memorizar los detalles internos —cada lenguaje incluye una implementación en la biblioteca estándar— pero ayuda saber el propósito: la clave se mezcla en el hash dos veces, de formas diferentes, para evitar un tipo de ataque llamado extensión de longitud.
Cómo utiliza un proveedor de webhooks el HMAC
Cuando registras un punto de entrada de webhook, el proveedor te proporciona un secreto de firma —una cadena aleatoria que solo tú y ellos conocen. Cuando ocurre un evento:
- El proveedor serializa el payload del evento a JSON (o a una cadena canónica).
- Calcula
HMAC-SHA256(secret, payload). - Envía la solicitud con la firma en el encabezado —
X-Hub-Signature-256para GitHub,Stripe-Signaturepara Stripe y así sucesivamente.
Desde tu lado, haces el mismo cálculo sobre el cuerpo original de la solicitud y la comparas. Si coinciden, el payload es auténtico. Si no, lo descartas.
Verificación en código
Aquí tienes cómo se ve la verificación en los lenguajes más comunes. El patrón es idéntico en todos: calcula, compara con una función de tiempo constante.
Node.js
const crypto = require('crypto');
function verifyWebhook(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody) // rawBody must be a Buffer or string — NOT parsed JSON
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
Pitón
import hmac
import hashlib
def verify_webhook(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
raw_body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
PHP
function verifyWebhook(string $rawBody, string $signature, string $secret): bool {
$expected = hash_hmac('sha256', $rawBody, $secret);
return hash_equals($expected, $signature);
}
El detalle crítico: usa el cuerpo original
El error más común en la implementación es pasar un payload analizado y re-serializado al función HMAC en lugar de los bytes originales. El orden de las claves, espacios en blanco y escape Unicode afectan al hash. El proveedor firmó los bytes exactos que envió por la red —debes hash exactamente esos mismos bytes.
En Express (Node.js), eso significa configurar el parser de cuerpo para preservar el buffer original:
app.use('/webhooks', express.raw({ type: 'application/json' }));
En Django, usa request.body en lugar de request.data. En Flask, usa request.get_data().
¿Por qué no un hash simple?
Un hash SHA-256 simple del payload demuestra nada —cualquiera puede calcularlo sin un secreto. El secreto en HMAC es lo que lo convierte en un SHA256(payload) código, no solo un checksum. Responde a «¿quién envió esto?» en lugar de simplemente «¿fue corrupto en tránsito?». autenticación ¿Por qué no firmas asimétricas (como RSA)?
RSA y ECDSA permiten que el receptor verifique la firma sin conocer la clave privada —eso es valioso para difusión pública (como firmas de código). Para webhooks, solo hay dos partes que necesitan verificar la firma: tú y el proveedor. Un secreto compartido es más sencillo, más rápido y igual de seguro en este modelo. Algunos proveedores (Svix, Clerk) ofrecen firma asimétrica para casos en que no puedes almacenar de forma segura un secreto en el servidor.
Ataques de retransmisión —y cómo detenerlos
Una firma HMAC válida demuestra autenticidad, pero no frescura. Un atacante que capture una solicitud firmada legítima puede retransmitirla más tarde. Stripe contrarresta esto incluyendo un timestamp en el
encabezado y hashando el timestamp junto con el cuerpo del payload. Desde tu lado, rechazas cualquier solicitud donde el timestamp sea más de cinco minutos de antiguo. Stripe-Signature Si estás construyendo tu propio sistema de webhooks, haz lo mismo: incluye un nonce creciente o un timestamp Unix en el mensaje firmado, y rechaza solicitudes obsoletas en el servidor.
La comparación segura en tiempo no es opcional
Nunca compares firmas HMAC con una comprobación de igualdad simple (
). La comparación de cadenas cortocircuita revela información sobre cuántos bytes iniciales coinciden —un atacante que haga miles de solicitudes puede reconstruir la firma esperada byte a byte. Siempre usa una comparación de tiempo constante:===, ==PHP:
- Node.js:
crypto.timingSafeEqual() - Python:
hmac.compare_digest() - Go:
hash_equals() - Ruby:
hmac.Equal() - Resumiendo: un manejador de webhook listo para producción
ActiveSupport::SecurityUtils.secure_compare()
Aquí tienes un ejemplo completo usando el encabezado de GitHub en Node.js:
Referencia rápida: quién usa qué X-Hub-Signature-256 Proveedor
const express = require('express');
const crypto = require('crypto');
const app = express();
const WEBHOOK_SECRET = process.env.GITHUB_WEBHOOK_SECRET;
// Keep the body as raw bytes — critical!
app.use('/github/webhook', express.raw({ type: 'application/json' }));
app.post('/github/webhook', (req, res) => {
const sigHeader = req.headers['x-hub-signature-256'];
if (!sigHeader) return res.status(401).send('Missing signature');
const sig = sigHeader.replace('sha256=', '');
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
// Safe to process the event now
console.log('Event type:', req.headers['x-github-event']);
res.sendStatus(200);
});
app.listen(3000);
Timestamp en la firma?
| X-Hub-Signature-256 | Encabezamiento | Algoritmo | Stripe-Signature |
|---|---|---|---|
| GitHub | X-Shopify-Hmac-Sha256 | HMAC-SHA256 | No |
| Stripe | X-Twilio-Signature | HMAC-SHA256 | Sí |
| Shopify | X-Slack-Signature | HMAC-SHA256 | No |
| Twilio | Paddle | HMAC-SHA1 | No |
| Flojo | Paddle-Signature | HMAC-SHA256 | Sí |
| HMAC: ¿Cómo saben los webhooks que no estás mintiendo 2? | HMAC: ¿Cómo saben los webhooks que no estás mintiendo 1? | HMAC-SHA256 | Sí |
También te puede interesar
Instalar extensiones
Agregue herramientas IO a su navegador favorito para obtener acceso instantáneo y búsquedas más rápidas
恵 ¡El marcador ha llegado!
Marcador es una forma divertida de llevar un registro de tus juegos, todos los datos se almacenan en tu navegador. ¡Próximamente habrá más funciones!
Herramientas clave
Ver todo Los recién llegados
Ver todoActualizar: Nuestro última herramienta se agregó el 27 abr. 2026
