Skip to main content

Visão Geral

Proteger seus endpoints de webhook é crítico para garantir que as solicitações de webhook venham do Gu1 e não de atores maliciosos. Este guia cobre como verificar assinaturas de webhook, implementar melhores práticas de segurança e evitar erros de segurança comuns.

Verificação de Assinatura

Gu1 assina todas as solicitações de webhook com uma assinatura HMAC SHA-256 usando seu secret de webhook. A assinatura é enviada no header X-Webhook-Signature, permitindo que você verifique se a solicitação é autêntica.

Como Funciona a Verificação de Assinatura

  1. Gu1 gera uma assinatura: Ao enviar um webhook, Gu1 cria um hash HMAC SHA-256 do corpo raw da solicitação usando seu secret de webhook
  2. Assinatura é enviada no header: A assinatura é incluída no header X-Webhook-Signature
  3. Seu servidor recalcula: Seu endpoint recalcula a assinatura usando o mesmo secret e corpo raw
  4. Comparar assinaturas: Se as assinaturas corresponderem, o webhook é autêntico
Sempre verifique assinaturas de webhook em produção. Sem verificação, qualquer pessoa pode enviar webhooks falsos para seu endpoint e potencialmente comprometer seu sistema.

Exemplos de Verificação de Assinatura

Node.js (Express)

const express = require('express');
const crypto = require('crypto');

const app = express();

// IMPORTANTE: Armazenar corpo raw para verificação de assinatura
app.use(express.json({
  verify: (req, res, buf) => {
    req.rawBody = buf.toString('utf8');
  }
}));

app.post('/webhooks/gu1', async (req, res) => {
  try {
    // Obter assinatura do header
    const signature = req.headers['x-webhook-signature'];
    const webhookSecret = process.env.GU1_WEBHOOK_SECRET;

    // Verificar assinatura
    if (!verifySignature(req.rawBody, signature, webhookSecret)) {
      console.error('Invalid webhook signature');
      return res.status(401).json({ error: 'Invalid signature' });
    }

    // Processar webhook
    const { event, timestamp, payload } = req.body;
    await handleWebhook(event, payload);

    // Retornar 200 para confirmar recebimento
    res.status(200).json({ success: true });
  } catch (error) {
    console.error('Webhook error:', error);
    res.status(500).json({ error: error.message });
  }
});

/**
 * Verificar assinatura HMAC SHA-256
 */
function verifySignature(rawBody, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  return signature === expectedSignature;
}

app.listen(3000);
Crítico: Você deve verificar a assinatura usando o corpo raw da solicitação antes de ser analisado como JSON. Se você verificar contra o corpo JSON analisado (por exemplo, JSON.stringify(req.body)), a assinatura não corresponderá porque a formatação JSON pode diferir.

Python (Flask)

from flask import Flask, request, jsonify
import hmac
import hashlib
import os
import logging

app = Flask(__name__)

@app.route('/webhooks/gu1', methods=['POST'])
def gu1_webhook():
    try:
        # Obter assinatura do header
        signature = request.headers.get('X-Webhook-Signature')
        webhook_secret = os.getenv('GU1_WEBHOOK_SECRET')

        # Obter corpo raw para verificação de assinatura
        raw_body = request.get_data(as_text=True)

        # Verificar assinatura
        if not verify_signature(raw_body, signature, webhook_secret):
            logging.error('Invalid webhook signature')
            return jsonify({'error': 'Invalid signature'}), 401

        # Analisar payload
        payload = request.json
        event = payload.get('event')
        data = payload.get('payload')

        logging.info(f'Received webhook: {event}')

        # Processar o 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)
        }), 500

def verify_signature(raw_body: str, signature: str, secret: str) -> bool:
    """Verificar assinatura 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):
    # Processar webhook baseado no tipo de evento
    pass

if __name__ == '__main__':
    app.run(port=3000)
Use hmac.compare_digest() em vez de == para comparar assinaturas em Python. Esta função realiza uma comparação segura contra timing que previne ataques de timing.

Go (Gin)

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "io/ioutil"
    "log"
    "net/http"
    "os"

    "github.com/gin-gonic/gin"
)

type WebhookPayload struct {
    Event     string                 `json:"event"`
    Timestamp string                 `json:"timestamp"`
    Payload   map[string]interface{} `json:"payload"`
}

func main() {
    r := gin.Default()
    r.POST("/webhooks/gu1", handleWebhook)
    r.Run(":3000")
}

func handleWebhook(c *gin.Context) {
    // Ler corpo raw para verificação de assinatura
    rawBody, err := ioutil.ReadAll(c.Request.Body)
    if err != nil {
        c.JSON(500, gin.H{"error": "Failed to read body"})
        return
    }

    // Verificar assinatura
    signature := c.GetHeader("X-Webhook-Signature")
    webhookSecret := os.Getenv("GU1_WEBHOOK_SECRET")

    if !verifySignature(rawBody, signature, webhookSecret) {
        log.Println("Invalid webhook signature")
        c.JSON(401, gin.H{"error": "Invalid signature"})
        return
    }

    // Analisar payload
    var payload WebhookPayload
    if err := json.Unmarshal(rawBody, &payload); err != nil {
        c.JSON(400, gin.H{"error": "Invalid JSON"})
        return
    }

    log.Printf("Received webhook: %s", payload.Event)

    // Processar webhook
    handleWebhookEvent(payload)

    c.JSON(200, gin.H{
        "success": true,
        "message": "Webhook received",
    })
}

func verifySignature(rawBody []byte, signature, secret string) bool {
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(rawBody)
    expectedSignature := hex.EncodeToString(mac.Sum(nil))

    return hmac.Equal([]byte(signature), []byte(expectedSignature))
}

func handleWebhookEvent(payload WebhookPayload) {
    // Processar webhook baseado no tipo de evento
}

Corpo Raw vs JSON Analisado

Um erro comum é verificar a assinatura usando o objeto JSON analisado em vez do corpo raw da solicitação. Isso sempre falhará porque a formatação JSON pode diferir.
// ERRADO: Verificando corpo analisado
app.post('/webhooks', express.json(), (req, res) => {
  const signature = req.headers['x-webhook-signature'];

  // Isso NÃO funcionará - JSON.stringify pode formatar diferentemente
  const body = JSON.stringify(req.body);
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex');

  if (signature === expectedSignature) {
    // Sempre falhará
  }
});

Padrões de Idempotência

Webhooks podem ser entregues mais de uma vez devido a problemas de rede, timeouts ou tentativas. Implemente idempotência para garantir que você processe cada webhook apenas uma vez.

Idempotência Baseada em Banco de Dados

Armazene IDs de webhook processados no seu banco de dados:
async function handleWebhook(webhook) {
  const webhookId = `${webhook.payload.entityId}_${webhook.event}_${webhook.timestamp}`;

  // Verificar se já foi processado
  const alreadyProcessed = await db.webhookLog.findUnique({
    where: { webhookId }
  });

  if (alreadyProcessed) {
    console.log('Webhook already processed, skipping');
    return; // Pular duplicado
  }

  // Processar webhook
  await processWebhookEvent(webhook);

  // Marcar como processado
  await db.webhookLog.create({
    data: {
      webhookId,
      event: webhook.event,
      processedAt: new Date(),
      payload: webhook
    }
  });
}

Idempotência Baseada em Cache

Para webhooks de alto volume, use um cache como Redis:
const Redis = require('ioredis');
const redis = new Redis();

async function handleWebhook(webhook) {
  const webhookId = `${webhook.payload.entityId}_${webhook.event}_${webhook.timestamp}`;

  // Tentar definir a chave com NX (apenas se não existir)
  const result = await redis.set(
    `webhook:${webhookId}`,
    '1',
    'EX', 86400, // Expira após 24 horas
    'NX'
  );

  if (!result) {
    console.log('Webhook already processed, skipping');
    return; // Já processado
  }

  // Processar webhook
  await processWebhookEvent(webhook);
}
import redis
import json

redis_client = redis.Redis(host='localhost', port=6379, db=0)

def handle_webhook(webhook):
    webhook_id = f"{webhook['payload']['entityId']}_{webhook['event']}_{webhook['timestamp']}"

    # Tentar definir a chave com NX (apenas se não existir)
    result = redis_client.set(
        f"webhook:{webhook_id}",
        "1",
        ex=86400,  # Expira após 24 horas
        nx=True
    )

    if not result:
        print('Webhook already processed, skipping')
        return  # Já processado

    # Processar webhook
    process_webhook_event(webhook)

Melhores Práticas de Segurança

Nunca pule a verificação de assinatura em ambientes de produção. Esta é sua defesa principal contra webhooks falsos.
// Sempre verificar a assinatura
if (!verifySignature(req.rawBody, signature, secret)) {
  return res.status(401).json({ error: 'Invalid signature' });
}
Configure seus endpoints de webhook para usar apenas HTTPS. Rejeite solicitações HTTP:
app.post('/webhooks', (req, res) => {
  // Verificar se a solicitação é HTTPS
  if (req.protocol !== 'https') {
    return res.status(403).json({ error: 'HTTPS required' });
  }

  // Processar webhook...
});
Processe apenas tipos de eventos que você está esperando:
const ALLOWED_EVENTS = [
  'entity.created',
  'entity.updated',
  'entity.status_changed',
  'kyc.validation_approved'
];

if (!ALLOWED_EVENTS.includes(webhook.event)) {
  console.warn('Unknown event type:', webhook.event);
  return res.status(400).json({ error: 'Unknown event type' });
}
Proteja seu endpoint contra abuso com limitação de taxa:
const rateLimit = require('express-rate-limit');

const webhookLimiter = rateLimit({
  windowMs: 1 * 60 * 1000, // 1 minuto
  max: 100, // Limitar cada IP a 100 solicitações por minuto
  message: 'Too many webhook requests'
});

app.post('/webhooks', webhookLimiter, handleWebhook);
Nunca codifique secrets de webhook. Use variáveis de ambiente ou gerenciamento de secrets:
// Bom - variável de ambiente
const secret = process.env.GU1_WEBHOOK_SECRET;

// Ruim - codificado
const secret = 'abc123'; // Nunca faça isso!
Para produção, use um gerenciador de secrets:
  • AWS Secrets Manager
  • HashiCorp Vault
  • Azure Key Vault
  • Google Secret Manager
Valide a estrutura do payload do webhook antes de processar:
function isValidEntityPayload(payload) {
  return (
    payload &&
    payload.entity &&
    payload.entity.id &&
    payload.entity.type
  );
}

if (!isValidEntityPayload(webhook.payload)) {
  return res.status(400).json({ error: 'Invalid payload structure' });
}
Ao comparar assinaturas, use funções de comparação seguras contra timing para prevenir ataques de timing:
const crypto = require('crypto');

function verifySignature(rawBody, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(rawBody)
    .digest('hex');

  // Comparação segura contra timing
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}
Registre todas as tentativas de webhook para auditoria e depuração:
async function logWebhook(webhook, success, error = null) {
  await db.webhookLog.create({
    data: {
      event: webhook.event,
      timestamp: webhook.timestamp,
      success,
      error: error?.message,
      payload: webhook,
      processedAt: new Date()
    }
  });
}
Responda com código de status 200 rapidamente para prevenir tentativas. Processe trabalho pesado assincronamente:
app.post('/webhooks', async (req, res) => {
  // Verificar assinatura primeiro
  if (!verifySignature(req.rawBody, signature, secret)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // Confirmar imediatamente
  res.status(200).json({ received: true });

  // Processar assincronamente
  processWebhook(req.body).catch(console.error);
});
Se o processamento de webhook falhar, armazene para tentativa:
try {
  await processWebhook(webhook);
} catch (error) {
  // Armazenar webhook falhado para tentativa
  await db.failedWebhook.create({
    data: {
      webhook,
      error: error.message,
      retryCount: 0,
      nextRetryAt: new Date(Date.now() + 5 * 60 * 1000) // Tentar em 5 minutos
    }
  });

  // Ainda retornar 200 para prevenir tentativas do Gu1
  res.status(200).json({ success: false });
}

Erros Comuns de Segurança a Evitar

Evite estes erros comuns de segurança que podem comprometer seus endpoints de webhook:

1. Pular Verificação de Assinatura

// NÃO FAÇA ISSO - Sem verificação de assinatura
app.post('/webhooks', (req, res) => {
  // Processando webhook sem verificação
  handleWebhook(req.body);
  res.status(200).send('OK');
});
Risco: Qualquer pessoa pode enviar webhooks falsos para seu endpoint.

2. Verificar JSON Analisado Em Vez de Corpo Raw

// NÃO FAÇA ISSO - Verificando corpo analisado
app.post('/webhooks', express.json(), (req, res) => {
  const signature = req.headers['x-webhook-signature'];
  const body = JSON.stringify(req.body); // Errado!

  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(body)
    .digest('hex');

  // Sempre falhará
});
Risco: A verificação de assinatura sempre falhará.

3. Usar HTTP Em Vez de HTTPS

// NÃO FAÇA ISSO - Aceitando HTTP
http://seuapp.com/webhooks
Risco: Payloads de webhook podem ser interceptados em trânsito.

4. Codificar Secrets

// NÃO FAÇA ISSO - Secret codificado
const secret = 'my-webhook-secret-123';
Risco: Secrets expostos no controle de versão ou logs.

5. Não Implementar Idempotência

// NÃO FAÇA ISSO - Processando duplicados
app.post('/webhooks', (req, res) => {
  // Sem verificação de duplicados
  await createUser(req.body.entity);
  res.status(200).send('OK');
});
Risco: Webhooks duplicados criarão registros duplicados.

6. Expor Erros aos Clientes

// NÃO FAÇA ISSO - Expondo erros internos
app.post('/webhooks', async (req, res) => {
  try {
    await processWebhook(req.body);
  } catch (error) {
    res.status(500).json({
      error: error.stack, // Expondo stack trace
      query: error.query  // Expondo query de banco de dados
    });
  }
});
Risco: Vazamento de informações internas para atacantes.

7. Não Validar Tipos de Eventos

// NÃO FAÇA ISSO - Processando eventos desconhecidos
app.post('/webhooks', (req, res) => {
  // Aceitando qualquer tipo de evento
  handleAnyEvent(req.body.event, req.body.payload);
});
Risco: Atacantes podem enviar tipos de eventos arbitrários.

8. Usar Secrets Fracos

// NÃO FAÇA ISSO - Secret fraco
const secret = '123456';
Risco: Secrets podem ser quebrados por força bruta. Solução: Use secrets fortes e gerados aleatoriamente (pelo menos 32 caracteres).

Testando Segurança de Webhook

Testar Assinaturas Inválidas

# Testar com assinatura inválida
curl -X POST https://seuapp.com/webhooks \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Signature: invalid-signature" \
  -d '{"event":"entity.created","payload":{}}'

# Deve retornar 401 Unauthorized

Testar Ataques de Replay

Envie o mesmo webhook duas vezes e verifique idempotência:
# Enviar webhook uma vez
curl -X POST https://seuapp.com/webhooks \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Signature: valid-signature" \
  -d '{"event":"entity.created","timestamp":"2025-01-07T10:00:00Z","payload":{"entity":{"id":"123"}}}'

# Enviar mesmo webhook novamente
curl -X POST https://seuapp.com/webhooks \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Signature: valid-signature" \
  -d '{"event":"entity.created","timestamp":"2025-01-07T10:00:00Z","payload":{"entity":{"id":"123"}}}'

# Segunda solicitação deve ser ignorada (idempotência)

Monitoramento e Alertas

Configure monitoramento para segurança de webhook:
// Rastrear métricas de webhook
const metrics = {
  totalReceived: 0,
  signatureVerified: 0,
  signatureFailed: 0,
  processed: 0,
  failed: 0
};

app.post('/webhooks', (req, res) => {
  metrics.totalReceived++;

  const signature = req.headers['x-webhook-signature'];
  if (!verifySignature(req.rawBody, signature, secret)) {
    metrics.signatureFailed++;

    // Alertar se muitas falhas de assinatura
    if (metrics.signatureFailed > 10) {
      alertSecurityTeam('High webhook signature failure rate');
    }

    return res.status(401).json({ error: 'Invalid signature' });
  }

  metrics.signatureVerified++;
  // Processar webhook...
});

Próximos Passos