SyncroCentral de Ajuda
No results found
Acessar Syncro

Assinatura HMAC

Updated on April 30, 2026

Cada webhook de saída do Syncro vem com assinatura HMAC SHA-256 no header X-Syncro-Signature. Validar assinatura garante que o request veio realmente do Syncro (não é hacker que descobriu sua URL e está mandando payload falso). Esse artigo cobre como funciona e como validar em PHP, Node.js e Python.

Por que validar

Sem validação, qualquer pessoa com a URL do seu endpoint pode mandar request fake:

curl -X POST "https://meusistema.com/webhook" \
 -H "Content-Type: application/json" \
 -d '{"event":"lead.won","data":{"value":1000000}}'

Sistema acha que é venda real, dispara fluxo (notifica Slack, atualiza dashboard) — fraude.

Com HMAC, request fake não tem assinatura válida, sistema rejeita.

Como funciona

Lado servidor (Syncro)

Ao disparar webhook:

  1. Calcula HMAC do body JSON com secret da subscription:
signature = HMAC_SHA256(body_json, secret)
  1. Adiciona header:
X-Syncro-Signature: sha256=<hex_digest>
  1. Faz POST.

Lado receptor (você)

Ao receber webhook:

  1. Lê header X-Syncro-Signature.
  2. body raw (não parseado — bytes exatos).
  3. Calcula HMAC esperado do body com mesmo secret.
  4. Compara header vs esperado com timing-safe comparison.
  5. Se igual → request legítimo, processe.
  6. Se diferente → rejeitar (401).

Onde tá o secret

Secret é gerado no momento da criação da subscription:

POST /api/v1/webhooks
{...}

Response:
{
 "id": 42,
 "secret": "abc123def456..." ← SALVE!
}

Salve agora — não aparece mais. Veja Webhooks de saída.

Validação em PHP

<?php

function validateWebhookSignature(string $body, string $secret, string $signatureHeader): bool
{
 // 1. Calcular HMAC esperado
 $expected = 'sha256='. hash_hmac('sha256', $body, $secret);

 // 2. Comparação timing-safe (previne timing attacks)
 return hash_equals($expected, $signatureHeader);
}

// Uso no endpoint
$secret = 'abc123def456...'; // salvo da criação
$signature = $_SERVER['HTTP_X_SYNCRO_SIGNATURE'] ?? '';
$body = file_get_contents('php://input'); // raw bytes

if (!validateWebhookSignature($body, $secret, $signature)) {
 http_response_code(401);
 echo json_encode(['error' => 'Invalid signature']);
 exit;
}

// Aprovado — pode parse e processar
$payload = json_decode($body, true);
$event = $payload['event'];
$data = $payload['data'];

//...processa...

http_response_code(200);
echo json_encode(['received' => true]);

⚠️ Atenção: use hash_equals (timing-safe), NÃO ===. Comparação simples permite timing attacks que vazam secret byte por byte.

Validação em Node.js

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

const app = express;

// IMPORTANTE: precisa do raw body pra HMAC, não JSON parseado
app.use(express.raw({ type: 'application/json' }));

const SECRET = 'abc123def456...';

function validateWebhookSignature(body, secret, signatureHeader) {
 const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');

 // Timing-safe comparison
 const expectedBuf = Buffer.from(expected);
 const actualBuf = Buffer.from(signatureHeader);

 // Se tamanhos diferentes, retorna false sem comparar
 if (expectedBuf.length !== actualBuf.length) {
 return false;
 }

 return crypto.timingSafeEqual(expectedBuf, actualBuf);
}

app.post('/webhook', (req, res) => {
 const signature = req.headers['x-syncro-signature'] || '';
 const body = req.body.toString('utf8');

 if (!validateWebhookSignature(body, SECRET, signature)) {
 return res.status(401).json({ error: 'Invalid signature' });
 }

 const payload = JSON.parse(body);
 console.log(`Event: ${payload.event}`);

 //...processa...

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

app.listen(3000,  => console.log('Listening on 3000'));

⚠️ Atenção: o express.raw é crítico. Se você usa express.json, o body já foi parseado e re-serializado — HMAC bate diferente.

Validação em Python (Flask)

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

app = Flask(__name__)

SECRET = b'abc123def456...' # bytes!

def validate_webhook_signature(body: bytes, secret: bytes, signature_header: str) -> bool:
 expected = 'sha256=' + hmac.new(
 secret,
 body,
 hashlib.sha256
 ).hexdigest

 # Timing-safe comparison
 return hmac.compare_digest(expected, signature_header)

@app.route('/webhook', methods=['POST'])
def webhook:
 body_raw = request.get_data # raw bytes
 signature = request.headers.get('X-Syncro-Signature', '')

 if not validate_webhook_signature(body_raw, SECRET, signature):
 return jsonify({'error': 'Invalid signature'}), 401

 payload = request.get_json
 print(f"Event: {payload['event']}")

 #...processa...

 return jsonify({'received': True}), 200

if __name__ == '__main__':
 app.run(port=3000)

⚠️ Atenção: secret deve ser bytes em Python (use b'...'). Se for str, sistema converte mas pode dar bug em encoding.

Validação em outros frameworks

Laravel (PHP)

Mesma lógica do PHP puro. Use middleware:

class VerifyWebhookSignature
{
 public function handle($request, Closure $next)
 {
 $secret = config('services.syncro.webhook_secret');
 $signature = $request->header('X-Syncro-Signature');
 $body = $request->getContent;

 $expected = 'sha256='. hash_hmac('sha256', $body, $secret);

 if (!hash_equals($expected, $signature)) {
 abort(401, 'Invalid signature');
 }

 return $next($request);
 }
}

// Aplique no route
Route::post('/webhook/syncro', WebhookController::class)
 ->middleware(VerifyWebhookSignature::class);

Django (Python)

import hmac
import hashlib
from django.http import JsonResponse
from django.views.decorators.csrf import csrf_exempt

@csrf_exempt
def webhook(request):
 secret = b'abc123...'
 signature = request.META.get('HTTP_X_SYNCRO_SIGNATURE', '')
 body = request.body # já é bytes

 expected = 'sha256=' + hmac.new(secret, body, hashlib.sha256).hexdigest

 if not hmac.compare_digest(expected, signature):
 return JsonResponse({'error': 'Invalid'}, status=401)

 payload = json.loads(body.decode('utf-8'))
 #...processa...

 return JsonResponse({'received': True})

Go

package main

import (
 "crypto/hmac"
 "crypto/sha256"
 "encoding/hex"
 "io/ioutil"
 "net/http"
)

const secret = "abc123def456..."

func validateSignature(body byte, signatureHeader string) bool {
 h:= hmac.New(sha256.New, byte(secret))
 h.Write(body)
 expected:= "sha256=" + hex.EncodeToString(h.Sum(nil))

 return hmac.Equal(byte(expected), byte(signatureHeader))
}

func handler(w http.ResponseWriter, r *http.Request) {
 body, _:= ioutil.ReadAll(r.Body)
 signature:= r.Header.Get("X-Syncro-Signature")

 if !validateSignature(body, signature) {
 http.Error(w, "Invalid signature", http.StatusUnauthorized)
 return
 }

 //...processa...
 w.WriteHeader(http.StatusOK)
}

Por que timing-safe?

Comparação simples (==):

"sha256=abc" == "sha256=xyz"

Compara byte por byte e para no primeiro diferente. Tempo de execução varia conforme posição do primeiro mismatch.

Hacker pode medir tempo de resposta:

  • Tentativa 1: sha256=aaa... → resposta em 5ms (mismatch no 1º char).
  • Tentativa 2: sha256=baa... → resposta em 10ms (mismatch no 2º char — passou pelo 1º).
  • Etc.

Aos poucos, descobre cada char do secret real. Em algumas centenas de tentativas, secret completo vazado.

Timing-safe comparison (hash_equals, hmac.compare_digest, crypto.timingSafeEqual):

  • Compara todos os bytes, mesmo após mismatch.
  • Tempo constante independente de quantos bytes batem.
  • Hacker não consegue inferir nada via timing.

Replay attacks

Mesmo com HMAC válido, request capturado pode ser reenviado:

  1. Hacker captura webhook legítimo (man-in-the-middle, log de servidor, etc).
  2. Reenvia 100x pro seu endpoint.
  3. Você processa 100x — pode causar dano (ex: notificação Slack 100x).

Mitigação 1 — Idempotência

Use X-Syncro-Delivery como dedup key:

$deliveryId = $_SERVER['HTTP_X_SYNCRO_DELIVERY'];

if (cache_has("processed:$deliveryId")) {
 return response->json(['received' => true]);
}

process_webhook($payload);
cache_put("processed:$deliveryId", true, 86400); // 24h

Mitigação 2 — Timestamp validation

Sistema inclui emitted_at ISO no payload:

{
 "event": "lead.created",
 "emitted_at": "2026-04-30T14:30:00Z",
 "data": {...}
}

Rejeite eventos antigos:

$emittedAt = new DateTime($payload['emitted_at']);
$now = new DateTime;
$diff = $now->getTimestamp - $emittedAt->getTimestamp;

if (abs($diff) > 300) { // 5 min
 return response->json(['error' => 'Event too old']);
}

💡 Dica: tolere clock skew (±5min) entre Syncro e seu servidor.

Não-validar — riscos

Se você não valida HMAC:

  • ✅ Funciona normalmente em produção.
  • ❌ Qualquer um descobre URL → manda request fake.
  • ❌ Sem proteção contra spoofing.
  • ❌ Auditoria/SOC2/LGPD vão reprovar.

Sempre valide em produção.

Testar localmente

Pra desenvolvimento local, pegue webhook real do Syncro:

  1. Configure subscription apontando pra serviço como webhook.site.
  2. Trigger evento (ex: crie lead).
  3. Veja request completo em webhook.site (headers + body).
  4. Copie o body raw.
  5. Calcule HMAC localmente:
echo -n '<body raw colado>' | openssl dgst -sha256 -hmac '<seu secret>'
  1. Compare com X-Syncro-Signature recebido.

Erros comuns

"Signature inválida sempre"

  • Body sendo modificado antes de validar (ex: middleware que adiciona char). Use raw body.
  • Secret errado.
  • Encoding diferente (UTF-8 vs UTF-16).
  • HMAC algoritmo diferente (SHA-512 ao invés de SHA-256).

"Funciona às vezes, falha outras"

  • Json reformatting: você está parseando e re-serializando body. Use raw.
  • Whitespace: body com \n extra. Use exato.

"Header X-Syncro-Signature não chega"

  • Você está usando middleware que strip headers.
  • Verifique config de proxy (nginx, cloudflare).

"Validação demora muito"

HMAC é muito rápido (~microssegundos). Se demorar segundos, problema é outro (load do servidor, network).

"Como sei o secret?"

Salvo no momento da criação da subscription. Se perdeu, delete e recrie subscription pra ganhar novo.

Boas práticas

1. Sempre raw body

Não parseie antes. HMAC roda em bytes exatos.

2. Use bibliotecas de timing-safe

hash_equals, hmac.compare_digest, crypto.timingSafeEqual. Nunca ===.

3. Secret forte e único

Sistema gera 40 chars random. Se quiser fornecer próprio, use mínimo 32 chars aleatórios.

4. Não logue secret

Logs podem vazar. Mantenha em vault/env vars.

5. Rotacione secret

Se suspeitar vazamento:

  1. Crie subscription nova com secret novo.
  2. Atualize seu sistema.
  3. Delete subscription antiga.

Próximos passos

Artigos relacionados