Salta al contenuto

Verifica della Firma del Webhook: Perché HMAC-SHA256 È l'Unico Modo Sicuro per Fidarsi di un Callback di Pagamento

Payora7 min di letturaEN · RU · UK · ES · DE · FR · IT · PL · PT · HI · ZH

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 è:

  1. Il gateway di pagamento conosce il tuo api_secret — un segreto condiviso solo tra te e il gateway.
  2. Prima di inviare il webhook, calcola: HMAC-SHA256(timestamp + "." + body, api_secret)
  3. Include questa firma e il timestamp negli header HTTP.
  4. 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

Pronto a iniziare?

Crea un account ed emetti la tua prima fattura in meno di un’ora.

Cosa risponde questa pagina