Webhook Handler com HMAC-SHA256 em Python Puro: O Verificador Que Impede Requisições Falsas (Sem Framework)
Seu Endpoint de Webhook Está Aberto Para Qualquer Um (E Você Nem Sabe)
Você configurou um webhook. Colocou a URL no painel do serviço externo. Recebeu o primeiro payload. Funcionou. Ótimo. Mas tem um problema que ninguém te conta: qualquer pessoa que descobrir essa URL pode enviar payloads falsos para o seu endpoint. E se o seu sistema confia cegamente no conteúdo recebido — como a maioria faz — você acabou de abrir uma porta dos fundos que nenhum firewall vai fechar.
Isso não é teoria. É o tipo de vulnerabilidade que aparece quando você menos espera: um concorrente descobre sua URL, um ex-funcionário com acesso antigo, ou simplesmente um bot que varre a internet atrás de endpoints desprotegidos. O resultado? Dados corrompidos, ações executadas indevidamente, e aquela reunião às 3h da manhã que ninguém pediu.
Neste artigo, vou te mostrar como construir um webhook handler com autenticação HMAC-SHA256 em Python puro. Sem Flask, sem Django, sem FastAPI, sem nenhuma dependência externa. Só a biblioteca padrão do Python fazendo o que ela faz de melhor: criptografia que funciona.
A keyword aqui é verificação HMAC de webhook — e ela precisa estar nos primeiros parágrafos porque é exatamente isso que vai separar seu endpoint de uma porta aberta para o caos.

O Que é HMAC e Por Que Seu Webhook Precisa Dele
HMAC (Hash-based Message Authentication Code) é um mecanismo criptográfico que permite verificar duas coisas simultaneamente:
- Autenticidade: a mensagem veio de quem diz que veio (porque só o remetente conhece a chave secreta).
- Integridade: a mensagem não foi alterada no caminho (qualquer bit modificado invalida o hash).
O funcionamento é elegante na sua simplicidade: o remetente calcula um hash SHA-256 do corpo da requisição usando uma chave secreta compartilhada e envia esse hash num header (geralmente X-Signature ou X-Hub-Signature-256). Você recebe a requisição, recalcula o hash com a mesma chave, e compara. Se bater, a requisição é legítima. Se não bater, você rejeita — sem processar, sem logar o payload, sem dar pistas ao atacante.
A maioria dos serviços que envia webhooks (GitHub, Stripe, Discord, Slack) já suporta HMAC nativamente. O problema é que nós, desenvolvedores, frequentemente ignoramos essa verificação porque “funciona sem ela”. É o equivalente digital de deixar a porta da frente aberta porque “o bairro é seguro”.
Arquitetura do Handler: HTTPServer Puro + Verificação Criptográfica
Em vez de importar 47 dependências para um servidor que só precisa ouvir uma porta, vamos usar o http.server da biblioteca padrão do Python. O handler vai:
- Receber a requisição POST
- Extrair a assinatura do header
- Ler o corpo da requisição (bytes crus, não string)
- Calcular HMAC-SHA256 com a chave secreta
- Comparar de forma segura (timing-safe) com a assinatura recebida
- Processar ou rejeitar
Vamos ao código. Mas antes, um aviso:
Box Perrengue: Na minha primeira versão, usei
==para comparar as assinaturas. Parece inocente, né? O problema é que comparações de string normais em Python são short-circuit — elas param no primeiro caractere diferente. Isso abre a porta para timing attacks: um atacante pode testar byte por byte e medir quanto tempo a comparação leva para deduzir a assinatura correta. A solução é usarhmac.compare_digest(), que sempre leva o mesmo tempo independente de onde os bytes diferem. Aprendi isso quando um colega me mostrou um PoC de 15 linhas. Levei três horas pra confiar que era real. Era.
Código Completo: Webhook Handler com HMAC-SHA256
Aqui está o handler completo. Copie, adapte, e nunca mais aceite webhooks sem verificação:
import hmac
import hashlib
import json
from http.server import HTTPServer, BaseHTTPRequestHandler
from datetime import datetime
# Chave secreta compartilhada com o remetente do webhook
# Na prática, use variável de ambiente — nunca hardcoded
WEBHOOK_SECRET = b"sua-chave-secreta-aqui-nao-hardcode"
class WebhookHandler(BaseHTTPRequestHandler):
"""Handler de webhook com verificação HMAC-SHA256."""
def do_POST(self):
# 1. Extrair assinatura do header
signature_header = self.headers.get("X-Signature", "")
if not signature_header:
self._respond(401, "Missing signature header")
return
# 2. Ler corpo da requisição (bytes crus!)
content_length = int(self.headers.get("Content-Length", 0))
body = self.rfile.read(content_length)
# 3. Calcular HMAC-SHA256 esperado
expected_signature = hmac.new(
WEBHOOK_SECRET,
body,
hashlib.sha256
).hexdigest()
# 4. Comparação timing-safe (NÃO use ==)
if not hmac.compare_digest(signature_header, expected_signature):
self._respond(403, "Invalid signature")
return
# 5. Verificação passou — processar payload
try:
payload = json.loads(body)
except json.JSONDecodeError:
self._respond(400, "Invalid JSON body")
return
# Processamento real do webhook
resultado = self._processar_evento(payload)
self._respond(200, json.dumps(resultado))
def _processar_evento(self, payload):
"""Lógica de processamento do webhook."""
evento = payload.get("event", "unknown")
timestamp = payload.get("timestamp", "")
print(f"[{datetime.now().isoformat()}] Evento: {evento}")
print(f" Timestamp: {timestamp}")
print(f" Payload keys: {list(payload.keys())}")
return {
"status": "received",
"event": evento,
"processed_at": datetime.now().isoformat()
}
def _respond(self, status_code, body):
"""Helper para enviar resposta HTTP."""
self.send_response(status_code)
self.send_header("Content-Type", "application/json")
self.end_headers()
if isinstance(body, str):
body = body.encode("utf-8")
self.wfile.write(body)
def log_message(self, format, *args):
"""Sobrescrever log para incluir info relevante."""
print(f"[WEBHOOK] {args[0]}")
if __name__ == "__main__":
PORT = 8443
server = HTTPServer(("0.0.0.0", PORT), WebhookHandler)
print(f"Webhook server listening on port {PORT}")
print(f"Secret configured: {'***' + WEBHOOK_SECRET[-4:].decode()}")
server.serve_forever()

Por Que Bytes Crus e Não String?
Um erro que vi muita gente cometer (e que eu cometi também) é decodificar o corpo para string antes de calcular o hash:
# ERRADO — decodificar antes do hash altera o resultado
body_str = body.decode("utf-8")
hmac.new(secret, body_str.encode("utf-8"), hashlib.sha256)
Parece que deveria ser a mesma coisa, mas não é. Dependendo da codificação, caracteres especiais, emojis, ou bytes de controle podem ser transformados durante a decodificação. O hash que você calcula vai ser diferente do hash que o remetente calculou — e a verificação vai falhar silenciosamente.
A regra é: hash sempre nos bytes crus. O remetente calculou o hash sobre os bytes exatos que enviou. Você deve calcular sobre os bytes exatos que recebeu. Ponto.
Como Configurar o Remetente do Webhook
Se você controla o serviço que envia os webhooks (por exemplo, um script Python que notifica outro sistema), aqui está como assinar:
import hmac
import hashlib
import requests
def enviar_webhook_assinado(url, payload, secret):
"""Envia webhook com assinatura HMAC-SHA256."""
body = json.dumps(payload).encode("utf-8")
# Calcular assinatura
signature = hmac.new(
secret.encode("utf-8"),
body,
hashlib.sha256
).hexdigest()
# Enviar com header de assinatura
response = requests.post(
url,
data=body,
headers={
"Content-Type": "application/json",
"X-Signature": signature,
},
timeout=10
)
return response
# Uso:
enviar_webhook_assinado(
"https://seu-servidor.com:8443/webhook",
{"event": "deploy.completed", "service": "api", "version": "2.4.1"},
"sua-chave-secreta-aqui-nao-hardcode"
)
Validando Webhooks de Serviços Externos (GitHub, Stripe, etc.)
Serviços populares usam formatos ligeiramente diferentes de assinatura. Aqui está como lidar com os principais:
GitHub
GitHub envia o header X-Hub-Signature-256 no formato sha256=HEX:
def validar_github(body, secret, header_value):
"""Valida webhook do GitHub (X-Hub-Signature-256)."""
if not header_value.startswith("sha256="):
return False
received_sig = header_value.split("=", 1)[1]
expected = hmac.new(
secret.encode("utf-8"),
body,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(received_sig, expected)
Stripe
Stripe usa Stripe-Signature com timestamp + múltiplas assinaturas:
def validar_stripe(payload_body, header, secret, tolerance=300):
"""Valida webhook do Stripe com tolerância de timestamp."""
import time
# Parse do header: t=TIMESTAMP,v1=ASSINATURA
parts = {}
for item in header.split(","):
key, val = item.split("=", 1)
parts[key] = val
timestamp = int(parts.get("t", 0))
if abs(time.time() - timestamp) > tolerance:
raise ValueError("Timestamp fora da janela permitida")
payload = f"{timestamp}.{payload_body.decode('utf-8')}"
expected = hmac.new(
secret.encode("utf-8"),
payload.encode("utf-8"),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(parts.get("v1", ""), expected)

Hardening: O Que Fazer Além do HMAC
HMAC-SHA256 resolve a autenticação, mas um endpoint de webhook em produção precisa de camadas extras:
1. Rotação de Chaves
Nunca use a mesma chave secreta para sempre. Implemente rotação periódica:
WEBHOOK_SECRETS = {
"v1": b"chave-antiga-ainda-valida-por-7-dias",
"v2": b"chave-nova-ativa-agora",
}
# Tente todas as versões válidas
for version, secret in WEBHOOK_SECRETS.items():
sig = hmac.new(secret, body, hashlib.sha256).hexdigest()
if hmac.compare_digest(received_signature, sig):
return True # Aceito por qualquer versão válida
return False # Nenhuma versão bateu
2. Idempotência
Webhooks podem ser reenviados. Se o mesmo evento chega duas vezes, seu sistema não pode processar duas vezes:
import sqlite3
db = sqlite3.connect("webhook_log.db")
db.execute("""
CREATE TABLE IF NOT EXISTS eventos_processados (
event_id TEXT PRIMARY KEY,
processed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
def ja_processado(event_id):
cursor = db.execute(
"SELECT 1 FROM eventos_processados WHERE event_id = ?",
(event_id,)
)
return cursor.fetchone() is not None
def marcar_processado(event_id):
db.execute(
"INSERT INTO eventos_processados (event_id) VALUES (?)",
(event_id,)
)
db.commit()
3. Rate Limiting por IP
Se alguém começar a spammar seu endpoint, você precisa de um freio:
import time
from collections import defaultdict
# Janela deslizante simples
REQUESTS_WINDOW = defaultdict(list)
MAX_REQUESTS_PER_MINUTE = 60
def rate_limit(ip):
now = time.time()
# Remove requisições mais antigas que 1 minuto
REQUESTS_WINDOW[ip] = [
t for t in REQUESTS_WINDOW[ip] if now - t < 60
]
if len(REQUESTS_WINDOW[ip]) >= MAX_REQUESTS_PER_MINUTE:
return False # Bloqueado
REQUESTS_WINDOW[ip].append(now)
return True # Permitido
Deploy com Systemd: Tornando Persistente
Seu handler não pode depender de um terminal aberto. Crie um serviço systemd:
cat > /etc/systemd/system/webhook-handler.service << EOF
[Unit]
Description=Webhook Handler com HMAC-SHA256
After=network.target
[Service]
Type=simple
User=webhook
Group=webhook
WorkingDirectory=/opt/webhook-handler
ExecStart=/usr/bin/python3 /opt/webhook-handler/webhook_server.py
Environment=WEBHOOK_SECRET=${WEBHOOK_SECRET}
Restart=on-failure
RestartSec=5
StandardOutput=journal
StandardError=journal
[Install]
WantedBy=multi-user.target
EOF
systemctl enable webhook-handler
systemctl start webhook-handler
systemctl status webhook-handler
Testando Localmente (Sem Expor o Servidor)
Para testar sem expor seu servidor para a internet, use um túnel reverso ou teste com curl local:
# Gerar assinatura de teste
BODY='{"event":"test","data":"hello"}'
SECRET="sua-chave-secreta-aqui-nao-hardcode"
SIGNATURE=$(echo -n "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | awk '{print $NF}')
# Enviar requisição assinada
curl -X POST http://localhost:8443/webhook \
-H "Content-Type: application/json" \
-H "X-Signature: $SIGNATURE" \
-d "$BODY"
Erros Que Já Cometi (Para Você Não Repetir)
Lição aprendida: Uma vez, coloquei o log do payload antes da verificação HMAC. Resultado: logs cheios de payloads de bots varrendo endpoints. O handler rejeitava tudo corretamente, mas meu disco enchia de lixo. Sempre valide antes de logar. O payload inválido não merece um byte no seu sistema de logging.
Outros erros comuns:
- Logar a chave secreta por acidente em modo debug (já vi isso em produção, dói admitir).
- Usar HTTP em vez de HTTPS — o HMAC protege contra falsificação, mas não contra sniffing. Se a chave vazar no caminho, o HMAC não adianta nada.
- Timeout infinito — sem timeout na leitura do corpo, um atacante pode enviar bytes lentamente e travar seu handler (slowloris para webhooks).
- Ignorar Content-Length — sem limite máximo, alguém pode enviar um payload de 500MB e estourar a memória.
Conclusão: Seu Webhook Merece Um Porteiro
Um webhook sem verificação HMAC é como uma casa com a campainha mas sem porta — qualquer um pode entrar. Implementar HMAC-SHA256 em Python puro leva menos de 50 linhas de código. Não precisa de framework, não precisa de dependência, não precisa de desculpa.
A diferença entre um sistema resiliente e um sistema que vai te acordar às 3h da manhã é, muitas vezes, essas poucas linhas de verificação que você adiou por “funcionar sem elas”.
Qual automação ou sistema de segurança você quer ver destrinchado aqui no Lab da Garra? Manda nos comentários — se for um problema real que eu já enfrentei (e eu já enfrentei muitos), o próximo artigo é seu.
Se esse conteúdo foi útil, confira também nossos artigos sobre scanner de vulnerabilidades com Python e circuit breaker com SQLite — dois padrões que complementam esse webhook handler na sua infraestrutura.
