Skip to Content
SmartVerifica della Sicurezza dei Webhook (HMAC Signature)

Verifica della Sicurezza dei Webhook (HMAC Signature)

Per garantire che ogni webhook ricevuto provenga realmente dal nostro sistema e non sia stato alterato o falsificato, utilizziamo il meccanismo standard di firma HMAC-SHA256, lo stesso adottato da Stripe, Shopify, GitHub, PayPal e molti altri servizi affidabili.

Principi di sicurezza garantiti

  • Autenticità — solo chi possiede la chiave segreta può generare una firma valida
  • Integrità — il contenuto del payload non è stato modificato durante il transito
  • Protezione da replay — grazie al timestamp, le richieste vecchie vengono rifiutate

Componenti coinvolti

Header HTTPDescrizioneObbligatorioEsempio valore
Webhook-TimestampTimestamp Unix (secondi) di generazione del webhook1733678400
Webhook-SignatureFirma nel formato t=<timestamp>,v1=<hmac_sha256_hex>t=1733678400,v1=4f8a9b2c...d7e1f3

Come calcoliamo la firma (lato nostro)

  1. Concateniamo: timestamp + ”.” + raw_body (il raw_body è il JSON esattamente come inviato, senza parsing o modifiche di formattazione)

  2. Calcoliamo l’HMAC-SHA256 utilizzando la chiave segreta condivisa con voi

  3. Convertiamo il risultato in stringa esadecimale minuscola

  4. Costruiamo l’header:

Webhook-Signature: t=<timestamp>,v1=<firma_calcolata>

Come verificare la firma (lato vostro – passi obbligatori)

  1. Controllare la presenza degli header
    Se Webhook-Timestamp o Webhook-Signature mancano → rispondere con 401 Unauthorized

  2. Verificare la freschezza del timestamp (anti-replay)

|current_time - timestamp| ≤ 300 secondi (tolleranza massima 5 minuti)

Se il timestamp è troppo vecchio → 401

  1. Estrarre la firma v1 Il valore di Webhook-Signature può contenere più firme separate da virgola (per supportare rotazione chiavi in futuro). Prendete solo la parte che inizia con v1=

    Esempio:

t=1733678400,v1=abcdef123456,v0=oldone → usare solo v1=abcdef123456
  1. Ricalcolare la firma attesa
payload_to_sign = timestamp + "." + request.getContent() // RAW body! expected = HMAC-SHA256(payload_to_sign, YOUR_WEBHOOK_SECRET).hexdigest()
  1. Confrontare in modo sicuro Utilizzare una funzione di comparazione timing-safe (es. hash_equals() in PHP, crypto.timingSafeEqual() in Node.js)

    Se non corrispondono esattamente → 401 Unauthorized

Esempio implementazione PHP

<?php function verifyWebhookSignature($request, $webhookSecret) { // 1. Ottenere gli header $timestamp = $request->header('Webhook-Timestamp'); $signature = $request->header('Webhook-Signature'); if (!$timestamp || !$signature) { return false; // 401 Unauthorized } // 2. Verificare freschezza del timestamp (max 5 minuti) $currentTime = time(); if (abs($currentTime - $timestamp) > 300) { return false; // Timestamp troppo vecchio } // 3. Estrarre la firma v1 preg_match('/v1=([a-f0-9]+)/', $signature, $matches); if (!isset($matches[1])) { return false; } $receivedSignature = $matches[1]; // 4. Ricalcolare la firma attesa $rawBody = $request->getContent(); // RAW body, non parsed! $payloadToSign = $timestamp . '.' . $rawBody; $expectedSignature = hash_hmac('sha256', $payloadToSign, $webhookSecret); // 5. Confrontare in modo timing-safe return hash_equals($expectedSignature, $receivedSignature); } // Utilizzo if (!verifyWebhookSignature($request, env('WEBHOOK_SECRET'))) { return response()->json(['error' => 'Invalid signature'], 401); } // Elaborare il webhook $payload = json_decode($request->getContent(), true); // ...

Esempio implementazione Node.js

const crypto = require('crypto'); function verifyWebhookSignature(req, webhookSecret) { // 1. Ottenere gli header const timestamp = req.headers['webhook-timestamp']; const signature = req.headers['webhook-signature']; if (!timestamp || !signature) { return false; // 401 Unauthorized } // 2. Verificare freschezza del timestamp (max 5 minuti) const currentTime = Math.floor(Date.now() / 1000); if (Math.abs(currentTime - parseInt(timestamp)) > 300) { return false; // Timestamp troppo vecchio } // 3. Estrarre la firma v1 const v1Match = signature.match(/v1=([a-f0-9]+)/); if (!v1Match) { return false; } const receivedSignature = v1Match[1]; // 4. Ricalcolare la firma attesa const rawBody = req.rawBody; // Deve essere il body RAW, non parsed! const payloadToSign = `${timestamp}.${rawBody}`; const expectedSignature = crypto .createHmac('sha256', webhookSecret) .update(payloadToSign) .digest('hex'); // 5. Confrontare in modo timing-safe return crypto.timingSafeEqual( Buffer.from(expectedSignature), Buffer.from(receivedSignature) ); } // Utilizzo con Express app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => { req.rawBody = req.body.toString('utf8'); if (!verifyWebhookSignature(req, process.env.WEBHOOK_SECRET)) { return res.status(401).json({ error: 'Invalid signature' }); } // Elaborare il webhook const payload = JSON.parse(req.rawBody); // ... res.json({ received: true }); });

Note importanti

  1. Usare sempre il body RAW: Non parsare il JSON prima di verificare la firma, altrimenti la formattazione potrebbe cambiare e la verifica fallirà

  2. Confronto timing-safe: Utilizzare sempre funzioni di comparazione sicure per prevenire timing attacks

  3. Tolleranza temporale: La finestra di 5 minuti è un buon compromesso tra sicurezza e tolleranza per problemi di sincronizzazione oraria

  4. Rotazione chiavi: Il formato con versione (v1=) permette di supportare più algoritmi di firma in futuro senza rompere la retrocompatibilità

  5. Logging: Registrare i tentativi falliti di verifica per individuare possibili attacchi

Payload del webhook

Il payload JSON inviato contiene informazioni sulla spedizione:

{ "event": "tracking.updated", "timestamp": 1733678400, "data": { "tracking_number": "ABC123456789", "carrier": "gls", "status": "in_transit", "status_description": "In transito", "last_update": "2024-12-08T10:30:00Z", "events": [ { "timestamp": "2024-12-08T10:30:00Z", "status": "in_transit", "location": "Milano", "description": "Pacco in transito" } ] } }

Eventi disponibili

EventoDescrizione
tracking.updatedAggiornamento stato spedizione
tracking.deliveredSpedizione consegnata
tracking.exceptionEccezione nella spedizione
shipment.createdNuova spedizione creata

Risposta richiesta

Il vostro endpoint deve rispondere con:

  • 200 OK se il webhook è stato elaborato con successo
  • 401 Unauthorized se la firma non è valida
  • 500 Internal Server Error in caso di errori nell’elaborazione

Il sistema riproverà automaticamente l’invio in caso di errore (con backoff esponenziale).

Last updated on