Un webhook di pagamento arriva al tuo server affermando che un ordine è stato pagato. Ti fidi? Se accrediti l'ordine basandoti solo sulla chiamata HTTP — senza verificare una firma crittografica — hai appena fornito agli attaccanti un modo gratuito per contrassegnare qualsiasi ordine come pagato.
Cosa Può Andare Storto Senza Verifica
Un endpoint webhook non protetto è facilmente sfruttabile. Un attaccante ha solo bisogno di conoscere il tuo URL di callback — che è spesso scoperto dalla scheda di rete di un browser, da un repository pubblico di GitHub o da un checkout di prova — e può POSTare un payload creato ad hoc:
POST /webhook HTTP/1.1
Content-Type: application/json
{"order_id": "1337", "status": "paid", "amount": "99.99"}Se il tuo gestore legge $_POST['status'] === 'paid' e accredita l'ordine, l'attaccante ha appena acquistato beni per $99.99 gratuitamente. Questo non è un attacco teorico; accade regolarmente contro integrazioni di pagamento non protette.
Come Funziona la Firma HMAC-SHA256
HMAC (Hash-based Message Authentication Code) con SHA-256 risolve questo problema rendendo il payload non falsificabile senza la chiave segreta. Il processo è:
- Il gateway di pagamento conosce il tuo api_secret — un segreto condiviso solo tra te e il gateway.
- Prima di inviare il webhook, calcola:
HMAC-SHA256(timestamp + "." + body, api_secret) - Include questa firma e il timestamp negli header HTTP.
- Il tuo server ricalcola lo stesso HMAC e confronta. Se corrispondono, il payload è autentico.
Un attaccante che non conosce il tuo api_secret non può falsificare una firma valida — SHA-256 è una funzione unidirezionale. Il timestamp previene attacchi di ripetizione: anche un webhook intercettato valido diventa inutile dopo 5 minuti.
Implementazione della Verifica: PHP
function verifyPayoraWebhook(string $secret): array {
$ts = $_SERVER['HTTP_X_PAYORA_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_PAYORA_SIGNATURE'] ?? '';
$body = file_get_contents('php://input');
// 1. rifiuta timestamp obsoleti (protezione da ripetizione)
if (!ctype_digit($ts) || abs(time() - (int)$ts) > 300) {
http_response_code(400); exit('timestamp obsoleto');
}
// 2. ricalcola e confronta — hash_equals() previene attacchi di timing
$expected = hash_hmac('sha256', $ts . '.' . $body, $secret);
if (!hash_equals($expected, $sig)) {
http_response_code(401); exit('firma non valida');
}
return json_decode($body, true);
}
$event = verifyPayoraWebhook('sk_your_secret');
if ($event['status'] === 'paid') {
creditOrder($event['order_id']);
}Implementazione della Verifica: Node.js
import crypto from 'node:crypto';
function verifyPayoraWebhook(req, rawBody, secret) {
const ts = req.headers['x-payora-timestamp'] ?? '';
const sig = req.headers['x-payora-signature'] ?? '';
if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) {
return null; // obsoleto
}
const expected = crypto
.createHmac('sha256', secret)
.update(`${ts}.${rawBody}`)
.digest('hex');
// timingSafeEqual previene attacchi di timing side-channel
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig))
? JSON.parse(rawBody)
: null;
}Errori Comuni Che Rompono la Verifica
1. Leggere un corpo analizzato invece dei byte grezzi. Il middleware json() di Express ri-serializza il JSON, il che può riordinare le chiavi o cambiare gli spazi bianchi. Sempre memorizza il corpo della richiesta grezzo prima di analizzare, e usalo per l'HMAC.
2. Usare === invece di un confronto sicuro per il timing. Un confronto di stringhe ingenuo rivela informazioni di timing che possono essere sfruttate per indovinare la firma byte per byte. Usa hash_equals() in PHP o crypto.timingSafeEqual() in Node.
3. Saltare il controllo del timestamp. Un webhook valido ma vecchio catturato da un osservatore di rete potrebbe essere ripetuto. La finestra di 5 minuti (±300 secondi) elimina questo rischio mentre gestisce un ragionevole scostamento dell'orologio.
4. Registrare il corpo grezzo prima della verifica. Se la tua pipeline di registrazione è compromessa, i payload registrati potrebbero essere ripetuti. Verifica prima, registra dopo.
Oltre la Firma: Idempotenza
La verifica dimostra che il webhook è autentico; l'idempotenza garantisce che non accrediti un ordine due volte. Le reti riprovano i webhook su risposte non-200, quindi il tuo gestore potrebbe ricevere lo stesso evento più volte. La soluzione è registrare un webhook_id nel tuo database e saltare l'elaborazione se l'hai già visto:
// pseudo-codice
$id = $event['webhook_id'];
if (DB::exists('processed_webhooks', $id)) return respond(200);
DB::insert('processed_webhooks', $id);
creditOrder($event['order_id']);Insieme, la verifica della firma e l'idempotenza ti offrono un'integrazione di pagamento sia sicura che resiliente. Vedi la guida completa all'integrazione per il riferimento completo ai webhook di Payora.
Comments