Asegura tus endpoints de webhook con verificación de firmas y mejores prácticas — con eventos webhook de gu1 para integración downstream en tiempo real.
Asegurar tus endpoints de webhook es crítico para garantizar que las solicitudes de webhook provienen de Gu1 y no de actores maliciosos. Esta guía cubre cómo verificar firmas de webhook, implementar mejores prácticas de seguridad y evitar errores comunes de seguridad.
Gu1 firma todas las solicitudes de webhook con una firma HMAC SHA-256 usando tu secreto de webhook. La firma se envía en el encabezado X-Webhook-Signature, permitiéndote verificar que la solicitud es auténtica.
Gu1 genera una firma: Al enviar un webhook, Gu1 crea un hash HMAC SHA-256 del cuerpo de la solicitud sin procesar usando tu secreto de webhook
La firma se envía en el encabezado: La firma se incluye en el encabezado X-Webhook-Signature
Tu servidor recalcula: Tu endpoint recalcula la firma usando el mismo secreto y cuerpo sin procesar
Comparar firmas: Si las firmas coinciden, el webhook es auténtico
Siempre verifica las firmas de webhook en producción. Sin verificación, cualquiera puede enviar webhooks falsos a tu endpoint y potencialmente comprometer tu sistema.
Crítico: Debes verificar la firma usando el cuerpo de solicitud sin procesar antes de que se analice como JSON. Si verificas contra el cuerpo JSON analizado (ej., JSON.stringify(req.body)), la firma no coincidirá porque el formato JSON puede diferir.
from flask import Flask, request, jsonifyimport hmacimport hashlibimport osimport loggingapp = Flask(__name__)@app.route('/webhooks/gu1', methods=['POST'])def gu1_webhook(): try: # Obtener firma del encabezado signature = request.headers.get('X-Webhook-Signature') webhook_secret = os.getenv('GU1_WEBHOOK_SECRET') # Obtener cuerpo sin procesar para verificación de firmas raw_body = request.get_data(as_text=True) # Verificar firma if not verify_signature(raw_body, signature, webhook_secret): logging.error('Invalid webhook signature') return jsonify({'error': 'Invalid signature'}), 401 # Analizar payload payload = request.json event = payload.get('event') data = payload.get('payload') logging.info(f'Received webhook: {event}') # Procesar el webhook handle_webhook(event, data) return jsonify({ 'success': True, 'message': 'Webhook received' }), 200 except Exception as e: logging.error(f'Webhook error: {e}') return jsonify({ 'success': False, 'error': str(e) }), 500def verify_signature(raw_body: str, signature: str, secret: str) -> bool: """Verificar firma HMAC SHA-256""" expected_signature = hmac.new( secret.encode('utf-8'), raw_body.encode('utf-8'), hashlib.sha256 ).hexdigest() return hmac.compare_digest(signature, expected_signature)def handle_webhook(event: str, data: dict): # Procesar webhook basado en tipo de evento passif __name__ == '__main__': app.run(port=3000)
Usa hmac.compare_digest() en lugar de == para comparar firmas en Python. Esta función realiza una comparación segura contra ataques de tiempo que previene ataques de temporización.
Un error común es verificar la firma usando el objeto JSON analizado en lugar del cuerpo de solicitud sin procesar. Esto siempre fallará porque el formato JSON puede diferir.
// INCORRECTO: Verificando cuerpo analizadoapp.post('/webhooks', express.json(), (req, res) => { const signature = req.headers['x-webhook-signature']; // Esto NO funcionará - JSON.stringify puede formatear diferente const body = JSON.stringify(req.body); const expectedSignature = crypto .createHmac('sha256', secret) .update(body) .digest('hex'); if (signature === expectedSignature) { // Siempre fallará }});
El monitor de webhooks muestra el payload para debugging y soporte. Esa vista no es el string byte-a-byte que se firmó al momento del envío. Pretty-print, copiar desde la UI o hacer JSON.stringify() sobre un objeto parseado puede generar un string distinto y hacer que un envío correcto parezca inválido.Para depurar un fallo, compará el header X-Webhook-Signature que recibió tu endpoint con el HMAC calculado sobre el raw body de ese mismo request HTTP. No re-verifiques solo con el JSON del dashboard.
Si la verificación funciona en algunos eventos pero en otros devuelve 401 Invalid signaturecon el mismo secret y endpoint, las causas habituales son:
JSON parseado y re-serializado — JSON.stringify(req.body) después de express.json() (o equivalente) no reproduce de forma confiable los bytes del body de Gu1. Payloads distintos (orden de keys, forma anidada, formato numérico) pueden fallar solo a veces.
Middleware que muta el body — deduplicar arrays, quitar campos null, ordenar keys o normalizar strings antes de verificar cambia el input firmado.
Secret incorrecto — secret regenerado en el dashboard mientras en tu entorno quedó el valor viejo.
Header de firma ausente — si el webhook no tiene secret configurado, Gu1 puede omitir X-Webhook-Signature; tratar un header faltante como firma inválida es esperable.
La verificación de firma es opcional pero recomendada. Gu1 igual entrega webhooks si no verificás; el 401 lo devuelve tu servidor cuando tu lógica de verificación rechaza el request.
Los webhooks pueden entregarse más de una vez debido a problemas de red, timeouts o reintentos. Implementa idempotencia para asegurar que procesas cada webhook solo una vez.
// NO HAGAS ESTO - Sin verificación de firmasapp.post('/webhooks', (req, res) => { // Procesando webhook sin verificación handleWebhook(req.body); res.status(200).send('OK');});
Riesgo: Cualquiera puede enviar webhooks falsos a tu endpoint.
// NO HAGAS ESTO - Procesando duplicadosapp.post('/webhooks', (req, res) => { // Sin verificación de duplicados await createUser(req.body.entity); res.status(200).send('OK');});
// NO HAGAS ESTO - Procesando eventos desconocidosapp.post('/webhooks', (req, res) => { // Aceptando cualquier tipo de evento handleAnyEvent(req.body.event, req.body.payload);});
Riesgo: Atacantes pueden enviar tipos de eventos arbitrarios.