Toda requisição de webhook do NueForm inclui uma assinatura criptográfica que permite verificar que a requisição é autêntica e não foi adulterada. Você deve sempre verificar essa assinatura antes de processar os dados do webhook.
Como a assinatura funciona
Quando o NueForm despacha um webhook, ele:
- Serializa o payload como uma string JSON.
- Calcula um hash HMAC-SHA256 dessa string JSON usando seu segredo do webhook como chave.
- Codifica o hash como uma string hexadecimal em letras minúsculas.
- Envia o digest hexadecimal no cabeçalho HTTP
X-NueForm-Signature.
Do seu lado, você executa o mesmo cálculo sobre o corpo bruto da requisição e compara o resultado com o valor do cabeçalho. Se coincidirem, a requisição é genuína.
O cabeçalho de assinatura
X-NueForm-Signature: 5d41402abc4b2a76b9719d911017c592a3f6e7d4b9c1d5e8f2a7b3c6d9e0f1a2
O valor do cabeçalho é uma string hexadecimal de 64 caracteres (a saída do HMAC-SHA256).
Seu segredo do webhook
Seu segredo do webhook é uma string hexadecimal de 64 caracteres gerada a partir de 32 bytes aleatórios. Ele é único da sua conta e compartilhado entre todos os seus endpoints de webhook (tanto por formulário quanto globais).
Obtendo seu segredo
Busque seu segredo atual pela API:
curl https://app.nueform.com/api/v1/webhooks/secret \
-H "Authorization: Bearer nf_your_api_key"
Resposta:
{
"secret": "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2"
}
Se você ainda não tiver um segredo, o NueForm gera um automaticamente na primeira requisição.
Regenerando seu segredo
Se o seu segredo for comprometido, regenere-o imediatamente:
curl -X POST https://app.nueform.com/api/v1/webhooks/secret/regenerate \
-H "Authorization: Bearer nf_your_api_key"
Regenerar seu segredo invalida imediatamente o antigo. Todas as entregas de webhook a partir desse momento usarão o novo segredo. Atualize seu código de verificação antes ou imediatamente depois de regenerar para evitar rejeitar webhooks válidos.
Exemplos de código de verificação
Node.js
import crypto from 'crypto';
function verifyWebhookSignature(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody)
.digest('hex');
// Use timing-safe comparison to prevent timing attacks
const a = Buffer.from(signature, 'hex');
const b = Buffer.from(expected, 'hex');
if (a.length !== b.length) {
return false;
}
return crypto.timingSafeEqual(a, b);
}
// Express.js example
app.post('/webhooks/nueform', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-nueform-signature'];
const rawBody = req.body.toString();
if (!verifyWebhookSignature(rawBody, signature, process.env.NUEFORM_WEBHOOK_SECRET)) {
console.error('Invalid webhook signature');
return res.status(401).send('Invalid signature');
}
const payload = JSON.parse(rawBody);
console.log('Verified webhook:', payload.event, payload.responseId);
// Process the webhook asynchronously
processWebhookAsync(payload);
res.status(200).send('OK');
});
Ao usar Express.js, você precisa usar express.raw() ou express.text() para acessar o corpo bruto da requisição. Se você usar express.json(), o corpo será interpretado e serializado novamente, o que pode produzir uma string diferente da que foi assinada --- fazendo a verificação falhar.
Python
import hashlib
import hmac
def verify_webhook_signature(raw_body: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode('utf-8'),
raw_body,
hashlib.sha256
).hexdigest()
# Use timing-safe comparison
return hmac.compare_digest(expected, signature)
# Flask example
from flask import Flask, request, abort
app = Flask(__name__)
@app.route('/webhooks/nueform', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-NueForm-Signature', '')
raw_body = request.get_data()
if not verify_webhook_signature(raw_body, signature, WEBHOOK_SECRET):
abort(401, 'Invalid signature')
payload = request.get_json()
print(f"Verified webhook: {payload['event']} {payload['responseId']}")
# Process asynchronously (e.g., enqueue to Celery)
process_webhook.delay(payload)
return 'OK', 200
Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
)
func verifyWebhookSignature(body []byte, signature string, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
// hmac.Equal performs a constant-time comparison
return hmac.Equal([]byte(expected), []byte(signature))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", http.StatusBadRequest)
return
}
defer r.Body.Close()
signature := r.Header.Get("X-NueForm-Signature")
if !verifyWebhookSignature(body, signature, webhookSecret) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process webhook payload
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
}
PHP
<?php
function verifyWebhookSignature(string $rawBody, string $signature, string $secret): bool {
$expected = hash_hmac('sha256', $rawBody, $secret);
// Use timing-safe comparison
return hash_equals($expected, $signature);
}
// Usage
$rawBody = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_NUEFORM_SIGNATURE'] ?? '';
$secret = getenv('NUEFORM_WEBHOOK_SECRET');
if (!verifyWebhookSignature($rawBody, $signature, $secret)) {
http_response_code(401);
echo 'Invalid signature';
exit;
}
$payload = json_decode($rawBody, true);
error_log("Verified webhook: {$payload['event']} {$payload['responseId']}");
// Process the webhook
processWebhook($payload);
http_response_code(200);
echo 'OK';
Ruby
require 'openssl'
require 'json'
def verify_webhook_signature(raw_body, signature, secret)
expected = OpenSSL::HMAC.hexdigest('SHA256', secret, raw_body)
# Use timing-safe comparison
Rack::Utils.secure_compare(expected, signature)
end
# Sinatra example
post '/webhooks/nueform' do
raw_body = request.body.read
signature = request.env['HTTP_X_NUEFORM_SIGNATURE'] || ''
unless verify_webhook_signature(raw_body, signature, ENV['NUEFORM_WEBHOOK_SECRET'])
halt 401, 'Invalid signature'
end
payload = JSON.parse(raw_body)
logger.info "Verified webhook: #{payload['event']} #{payload['responseId']}"
# Process asynchronously
WebhookProcessorJob.perform_async(payload)
status 200
body 'OK'
end
Comparação em tempo constante
Todos os exemplos de código acima usam comparação em tempo constante (também chamada de comparação timing-safe) ao verificar assinaturas. Essa é uma boa prática de segurança que previne ataques de temporização.
Um ataque de temporização funciona medindo quanto tempo uma comparação de strings leva. Uma comparação ingênua com == retorna false assim que encontra o primeiro caractere divergente, então um invasor poderia descobrir um caractere da assinatura por vez medindo a latência das respostas.
Funções de comparação em tempo constante sempre levam o mesmo tempo, independentemente de quantos caracteres coincidem, tornando esse ataque inviável.
| Linguagem | Função |
|---|---|
| Node.js | crypto.timingSafeEqual() |
| Python | hmac.compare_digest() |
| Go | hmac.Equal() |
| PHP | hash_equals() |
| Ruby | Rack::Utils.secure_compare() |
Nunca use ===, == ou .equals() para comparar assinaturas HMAC. Sempre use a função de comparação em tempo constante nativa da sua linguagem.
O que fazer se a verificação falhar
Se a verificação de assinatura falhar, seu endpoint deve:
- Retornar um código de status
401 Unauthorized. Não processe o payload. - Registrar a falha para depuração. Inclua o IP da requisição, o timestamp e (opcionalmente) a assinatura recebida.
- Não expor seu segredo em mensagens de erro ou logs.
- Investigar as causas comuns:
- Segredo errado. Certifique-se de estar usando o segredo do webhook atual. Se você o regenerou recentemente, atualize seu código de verificação.
- Transformação do corpo. Garanta que você está verificando o corpo bruto da requisição, não uma versão interpretada e serializada novamente. Middleware que faz o parse do JSON antes de o seu código de verificação executar é a causa mais comum de falhas.
- Problemas de codificação. O corpo bruto deve ser tratado como bytes UTF-8. Garanta que seu framework não aplique transformações de codificação inesperadas.
- Modificação por proxy. Se um proxy reverso (por exemplo, Cloudflare, nginx) estiver modificando o corpo da requisição, a assinatura não vai coincidir. Configure seu proxy para repassar o corpo sem alterações.
Checklist de solução de problemas
| Problema | Solução |
|---|---|
| A assinatura nunca coincide | Verifique se você está lendo os bytes brutos do corpo, não um objeto JSON interpretado |
| A assinatura parou de coincidir de repente | Verifique se seu segredo do webhook foi regenerado |
| A assinatura coincide localmente, mas não em produção | Verifique se há transformação do corpo por proxy ou CDN |
O cabeçalho X-NueForm-Signature está ausente | Garanta que seu framework preserve cabeçalhos personalizados (alguns removem cabeçalhos com prefixo X-) |