¿Odias los anuncios? Ir Sin publicidad Hoy

Firmas de Webhook Verificar cargas y detener solicitudes falsas

Actualizado en

Aprenda cómo funcionan las firmas de webhooks HMAC-SHA256, cómo implementar la verificación en tiempo constante en Python, Node.js y PHP, y los errores comunes que causan fallos silenciosos en producción.

Firmas de Webhook: Verifica los payloads y detén solicitudes falsas 1
ANUNCIO · ¿ELIMINAR?

Cualquiera puede enviar una solicitud POST a tu punto final de webhook. Sin verificación de firma, tu servidor no tiene forma de saber si esa solicitud realmente proviene de Stripe, GitHub o de tu infraestructura propia — o de un atacante que esté retransmitiendo un evento legítimo.

La verificación de firma de webhook resuelve esto. Es cómo los procesadores de pagos, las plataformas de control de versiones y los sistemas de comercio electrónico te permiten probar la autenticidad antes de actuar sobre los datos recibidos. Este artículo explica cómo funcionan las firmas HMAC-SHA256, el patrón paso a paso de verificación y los errores sutiles que causan fallos en producción.

¿Por qué los webhooks no autenticados son un riesgo de seguridad?

Un punto final de webhook es simplemente un manejador HTTP expuesto a internet. Sin verificación, cualquier solicitud que lo impacte será procesada. Dos tipos de ataque destacan:

  • Falsificación — un atacante crea un payload falso que parece un evento legítimo (una transacción exitosa, una suscripción renovada) y desencadena acciones en tu sistema sin que ocurra ninguna transacción real.
  • Ataques de retransmisión — un pedido legítimo es capturado durante la transmisión y reenviado más tarde. Si tu punto final es idempotente pero no protegido, el mismo evento se activa varias veces.

Ambos ataques se previenen mediante un secreto compartido combinado con una firma criptográfica. El remitente firma el payload; tú verificas la firma antes de procesar cualquier cosa.

Cómo funcionan las firmas HMAC-SHA256

HMAC (Código de autenticación basado en hash) toma dos entradas: una clave secreta y un mensaje. Lo procesa a través de una función de hash — SHA-256 en la mayoría de las implementaciones de webhook — y produce una firma de longitud fija.

La propiedad clave: el mismo secreto + mensaje siempre producen la misma firma, y cambiar incluso un solo byte en el mensaje produce una salida completamente diferente. Cualquiera que no tenga la clave secreta no puede producir una firma válida, incluso si puede ver el payload completo.

En la práctica, el flujo es así:

  1. Registra tu webhook con un servicio (por ejemplo, Stripe). El servicio te da una secreto de firma.
  2. Cuando el servicio envía un evento, calcula HMAC-SHA256(secret, payload) y lo incluye en una cabecera de solicitud.
  3. Tu servidor recibe la solicitud, calcula el mismo HMAC usando tu clave almacenada y el cuerpo original de la solicitud, luego compara las dos firmas.
  4. Si coinciden, la solicitud es auténtica. Si no, la rechaza.

El patrón de verificación, paso a paso

Aquí hay una implementación en Python que refleja lo que esperan las principales plataformas:

import hmac
import hashlib

def verify_signature(payload: bytes, secret: str, received_sig: str) -> bool:
    expected = hmac.new(
        key=secret.encode("utf-8"),
        msg=payload,
        digestmod=hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, received_sig)

Dos cosas importan aquí más allá de la llamada HMAC. Primero: payload es bytes, no una cadena decodificada — debes pasar el cuerpo original de la solicitud exactamente como lo recibes de la red. Segundo: la comparación utiliza hmac.compare_digest en lugar de ==. Esto es intencional.

¿Por qué la comparación de tiempo constante no es opcional?

La comparación de cadenas en la mayoría de los lenguajes se detiene: devuelve false al momento que encuentra un carácter diferente. Un atacante que pueda medir el tiempo de respuesta puede explotar esto — enviando miles de solicitudes con firmas variadas y usando diferencias de tiempo para adivinar el valor correcto carácter por carácter. Esto es un ataque de tiempo.

hmac.compare_digest en Python, hash_equals en PHP, y crypto.timingSafeEqual en Node.js todos tardan el mismo tiempo independientemente de dónde se diferencian las cadenas. Usa ellos cada vez que compares una firma — sin excepciones.

Formatos de cabecera en el mundo real: Stripe, GitHub, Shopify

Cada servicio tiene un nombre de cabecera y una codificación de firma ligeramente diferente. Aquí se muestra lo que debes analizar para las tres integraciones más comunes.

Stripe

Stripe envía una Stripe-Signature cabecera con este formato:

Stripe-Signature: t=1492774577,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a05bd539e6d5b9d2a2d2fbe

El t el campo es el timestamp Unix del momento en que se envió el evento. El v1 valor es el HMAC-SHA256 de timestamp + "." + raw_body. Al implementarlo manualmente:

  1. Analiza la cabecera para extraer t y v1.
  2. Calcula HMAC-SHA256(secret, t + "." + raw_body).
  3. Compara con v1 usando una comparación de tiempo constante.
  4. Rechaza si t es mayor de 300 segundos (5 minutos) respecto al tiempo actual.

GitHub

GitHub utiliza la X-Hub-Signature-256 encabezado:

X-Hub-Signature-256: sha256=6ffbb59b2300aae63f272406069a9788598b770f698a48021f99b32f8de06bb3

Elimina el prefijo sha256= luego compara el resto contra tu HMAC del cuerpo original. GitHub no incluye un timestamp en el contenido firmado, por lo que debes implementar deduplicación de eventos por separado si el protección contra retransmisión es importante para tu caso de uso.

Shopify

Shopify utiliza X-Shopify-Hmac-Sha256 con el digest codificado en Base64 en lugar de hex:

X-Shopify-Hmac-Sha256: b6LPiZidmXnJQf0Ff/p7MZQPIBPN9TqAqLMgAfn2YLQ=

Decodifica tanto tu HMAC calculado como el valor de la cabecera de Base64 a bytes brutos, luego compara con una función de comparación de tiempo constante. Comparar cadenas en Base64 directamente puede introducir errores sutiles de normalización de codificación.

El error más común: cuerpo bruto versus JSON decodificado

Es aquí donde más fallas en la verificación de firmas de webhook ocurren en producción. Cuando tu marco automatiza la decodificación del cuerpo de la solicitud en un diccionario o objeto antes de que tu manejador se ejecute, los bytes brutos se pierden. El HMAC se calculó sobre esos bytes originales — no sobre lo que produce tu biblioteca de JSON al re-serializar la estructura decodificada.

Aunque los datos sean semanticamente idénticos, {"amount": 100} y {"amount":100} (sin espacio después de la coma) producen valores HMAC diferentes. Diferencias en el orden de los campos, normalización de Unicode y precisión de flotantes pueden romper la verificación silenciosamente sin errores evidentes.

La solución: almacena el cuerpo bruto de la solicitud antes de cualquier decodificación y pasa esos bytes a tu función de verificación. En Express.js, registra la ruta con express.raw() middleware en lugar de express.json():

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const rawBody = req.body; // Buffer — not a parsed object
  const sig = req.headers['stripe-signature'];
  // Verify rawBody against sig before JSON.parse()
});

En Django, usa request.body (bytes). En Laravel, utiliza $request->getContent(). El patrón es consistente: accede directamente a los bytes brutos del pedido, nunca re-serializa una representación decodificada.

Protección contra retransmisión con timestamps

Una firma válida solo prueba que el payload no fue alterado durante la transmisión. No previene que un atacante capture una solicitud legítima y la reenvíe horas después — la firma seguirá verificándose correctamente porque nada ha cambiado.

Stripe aborda esto incluyendo un timestamp Unix en el payload firmado y recomienda un margen de tolerancia de 5 minutos. Después de extraer t de la Stripe-Signature cabecera, rechaza las solicitudes donde el timestamp esté fuera de ese margen:

import time

def is_within_tolerance(timestamp: int, tolerance_seconds: int = 300) -> bool:
    return abs(time.time() - timestamp) <= tolerance_seconds

# In your handler:
if not is_within_tolerance(stripe_timestamp):
    return HttpResponse(status=403)  # Reject stale request

Para servicios como GitHub y Shopify que no incluyen timestamps en su esquema de firma, implementa tu propia protección contra retransmisión almacenando IDs de eventos procesados (disponibles en el payload) y rechazando cualquier ID que ya hayas manejado. Un caché breve o un conjunto en Redis con un TTL que coincida con tu ventana de procesamiento funciona bien.

Verifica firmas sin escribir código

Al depurar una integración de webhook — o auditar un payload que ya has recibido — el Validador de Firma Webhook te permite pegar un payload, una clave secreta y una firma recibida para verificar inmediatamente sin necesidad de arrancar un entorno local.

Para generar firmas HMAC para pruebas de tu punto final con payloads diseñados, utiliza el Generador HMAC. Produce salidas en hex y Base64 para SHA-256, SHA-512 y varios otros algoritmos de digest — útiles para construir casos de prueba que cubran los formatos utilizados por cada servicio.

Guía rápida

  • El esquema de firma HMAC-SHA256 con una clave compartida es el estándar en Stripe, GitHub, Shopify y la mayoría de otros servicios.
  • Siempre utiliza una comparación de tiempo constante (hmac.compare_digest, hash_equals, crypto.timingSafeEqual) — no ==.
  • Pasa los bytes brutos del cuerpo de la solicitud a tu función HMAC, nunca un objeto re-serializado en JSON.
  • Verifica el timestamp en los servicios que lo incluyen; el margen de tolerancia de Stripe es de 5 minutos.
  • Deduplica por ID de evento en los servicios que no tienen protección contra retransmisión basada en timestamp.
  • El nombre de la cabecera y la codificación (hex vs Base64) varía por servicio — siempre revisa las documentaciones de integración.
¿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?