¿Odias los anuncios? Ir Sin publicidad Hoy

HMAC — Cómo los webhooks saben que no estás mintiendo

Publicado el

Cualquier servidor puede enviar una solicitud POST a tu punto de conexión de webhook. Las firmas HMAC son la forma en que los enviadores legítimos demuestran que escribieron el contenido — y cómo tú lo verificas.

HMAC — Cómo los webhooks saben que no estás mintiendo 1
ANUNCIO · ¿ELIMINAR?

Cada vez que Stripe carga una tarjeta, dispara un webhook. Cada vez que GitHub fusiona una solicitud, dispara un webhook. Estas plataformas envían solicitudes POST a una URL que usted les proporcionó — pero aquí está la verdad incómoda: cualquier otra persona puede enviar una solicitud POST a esa misma URL.

Entonces, ¿cómo sabe su servidor que la solicitud realmente proviene de Stripe y no de un atacante que adivinó la URL de su punto final? La respuesta es HMAC — y una vez que lo entienda, verá por qué es el enfoque estándar en todas las plataformas de API serias.

El problema: cualquiera puede enviar un POST a su punto final

Los puntos finales de webhooks son simplemente URLs. Son públicamente accesibles (deben serlo, para que el remitente pueda alcanzarlos) y aceptan solicitudes POST. No hay nada que impida que un agresor malicioso cree un payload falso y lo envíe a su punto final.

Imagina que tu manejador de webhooks hace esto cuando recibe un evento:

if event["type"] == "payment.completed":
    fulfill_order(event["data"]["order_id"])

Un atacante que conoce tu URL de punto final podría enviar un evento falso con cualquier ID de pedido que desee. payment.completed Sin verificación, tu servidor aceptaría órdenes que nunca habrían sido pagadas.

Necesitas una forma de verificar que el payload fue escrito por alguien que posee un secreto que ambos comparten — sin transmitir ese secreto en la solicitud.

¿Qué es HMAC?

HMAC significa Código de autenticación de mensaje basado en hash. Es una construcción que combina una función de hash criptográfica (normalmente SHA-256) con una clave secreta para producir una firma. La firma demuestra dos cosas:

  • Autenticidad — el mensaje fue creado por alguien que posee la clave secreta
  • Integridad — el mensaje no ha sido modificado durante la transmisión

La propiedad clave del HMAC: no puedes producir una firma válida sin conocer el secreto. Y no puedes revertir la firma para recuperar el secreto. Es una prueba unidireccional.

HMAC frente a un hash simple

Un hash simple (como SHA-256) del payload resuelve el problema de integridad, pero no de autenticidad. Un atacante que intercepta un payload válido podría recalcula el hash de un payload modificado. El HMAC mezcla tu clave secreta en cada paso del proceso de hashing, por lo que sin la clave, no puedes producir una firma coincidente incluso si conoces el algoritmo de hash utilizado.

Cómo funciona el HMAC de webhooks

El flujo tiene tres pasos: intercambio de clave, firma y verificación.

Paso 1: Intercambio de clave (ocurre una vez)

Cuando configuras un webhook con una plataforma como Stripe o GitHub, ellos generan un secreto de webhook y te lo muestran una vez. Lo almacenas en el servidor (nunca en código del cliente ni en repositorios públicos). Eso es todo — el secreto nunca viaja por el cable nuevamente.

Paso 2: El remitente firma el payload

Antes de enviar el webhook, la plataforma calcula una firma HMAC sobre el cuerpo original de la solicitud utilizando tu clave compartida:

signature = HMAC-SHA256(secret_key, request_body)

La firma se adjunta luego a la solicitud, normalmente en una cabecera como X-Hub-Signature-256 (GitHub) o Stripe-Signature (Stripe). El cuerpo original del payload viaja en el cuerpo sin cambios.

Paso 3: Verificación al recibir la solicitud

Cuando tu servidor recibe el webhook, recalculas el mismo HMAC utilizando el cuerpo original y tu clave almacenada, luego comparas el resultado con la firma en la cabecera. Si coinciden, el payload es auténtico y no modificado. Si no, lo rechazas.

expected = HMAC-SHA256(your_secret, raw_body)
if not constant_time_equal(expected, header_signature):
    return 401

Aviso comparación de tiempo constante — volveremos a explicar por qué eso importa.

Ejemplos del Mundo Real

Stripe

Stripe envía una Stripe-Signature cabecera que contiene un timestamp y una o más firmas:

Stripe-Signature: t=1679000000,v1=abc123...,v0=oldformat...

La firma se calcula sobre timestamp.payload (concatenada con un punto). Incluir el timestamp permite a Stripe defenderse contra ataques de retransmisión — si un atacante captura una solicitud firmada válida y la reenvía más tarde, puedes rechazarla porque el timestamp es demasiado antiguo.

GitHub

GitHub envía una X-Hub-Signature-256 cabecera en el formato sha256=<hex_digest>. La firma es HMAC-SHA256 del cuerpo original utilizando la clave de webhook que configuraste en las configuraciones de tu repositorio.

Shopify

Shopify utiliza una X-Shopify-Hmac-Sha256 cabecera con una firma HMAC-SHA256 codificada en Base64 — mismo concepto, diferente codificación.

Verificación en código

Aquí cómo se ve la verificación en tres lenguajes comunes. El patrón es idéntico — solo difieren las llamadas a la biblioteca.

Pitón

import hmac
import hashlib

def verify_webhook(secret: str, payload: bytes, signature_header: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()
    
    received = signature_header.removeprefix("sha256=")
    return hmac.compare_digest(expected, received)

Node.js

const crypto = require('crypto');

function verifyWebhook(secret, rawBody, signatureHeader) {
  const expected = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');
  
  const received = signatureHeader.replace('sha256=', '');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(received)
  );
}

PHP

function verifyWebhook(string $secret, string $rawBody, string $signatureHeader): bool {
    $expected = 'sha256=' . hash_hmac('sha256', $rawBody, $secret);
    return hash_equals($expected, $signatureHeader);
}

Errores que rompen la seguridad

1. Usar == en lugar de comparación de tiempo constante

Comparación estándar de cadenas (==, ===) corta el circuito tan pronto como encuentra una desigualdad. Esto crea un canal de tiempo lateral: un atacante puede medir cuánto tiempo tarda tu servidor en rechazar diferentes firmas. Las cadenas que comparten un prefijo más largo tardan ligeramente más en ser rechazadas. Con suficientes solicitudes, un atacante puede usar esto para reconstruir una firma válida byte por byte.

Siempre usa una comparación de tiempo constante: hmac.compare_digest() en Python, crypto.timingSafeEqual() en Node.js, hash_equals() en PHP.

2. Analizar el cuerpo antes de verificar

El HMAC se calcula sobre los bytes brutos del cuerpo de la solicitud. Si analizas el JSON primero y luego lo re-serializas para verificar, podrías obtener una secuencia de bytes diferente (diferente orden de claves, espacios en blanco, codificación). Siempre captura el cuerpo bruto antes que el analizador de cuerpo de tu framework lo toque, luego verifica contra ese.

3. No verificar ataques de retransmisión

Una solicitud firmada válida es válida para siempre — a menos que verifiques el timestamp. Si una plataforma incluye un timestamp en su esquema de firma (Stripe lo hace; GitHub no), rechaza las solicitudes donde el timestamp es más antiguo que unos minutos. Esto previene que un atacante capture y retransmita una solicitud legítima.

4. Incluir el secreto directamente en el código fuente

Los secretos de webhook deben vivir en variables de entorno o en un gestor de secretos, nunca cometidos en control de versiones. Si se revela un secreto, un atacante puede falsificar cualquier payload para siempre — hasta que lo rotas.

Lo que HMAC no protege

HMAC demuestra que el payload fue firmado por alguien que posee el secreto. No protege contra: no — un remitente comprometido

  • — si la infraestructura de firma de Stripe fuera comprometida, los eventos falsos aún tendrían firmas válidas — a menos que también valides un timestamp o un nonce
  • Ataques de retransmisión — la confidencialidad
  • — HMAC no cifra nada; el payload viaja en texto plano (aunque HTTPS lo maneja) Para la mayoría de las integraciones de webhooks, HMAC sobre HTTPS cubre todo lo que necesitas.

HMAC — Cómo los webhooks saben que no estás mintiendo 2

¿Quieres eliminar publicidad? Adiós publicidad hoy

Instalar extensiones

Agregue herramientas IO a su navegador favorito para obtener acceso instantáneo y búsquedas más rápidas

añadir Extensión de Chrome añadir Extensión de borde añadir Extensión de Firefox añadir Extensión de Opera

¡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!

ANUNCIO · ¿ELIMINAR?
ANUNCIO · ¿ELIMINAR?
ANUNCIO · ¿ELIMINAR?

Noticias Aspectos técnicos clave

Involucrarse

Ayúdanos a seguir brindando valiosas herramientas gratuitas

Invítame a un café
ANUNCIO · ¿ELIMINAR?