Skip to Content
SmartWebhooks

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 webhookSì1733678400
Webhook-SignatureFirma nel formato t=<timestamp>,v1=<hmac_sha256_hex>Sì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

Contenuto degli invii

Il contenuto di ogni evento, con esempi, è descritto in Webhook . Esempio di tracking.update:

{ "event": "tracking.update", "timestamp": "2026-10-01T08:15:02+02:00", "ldv": "3UW0WTQ069239", "vector_name": "PosteDeliveryBusiness", "order_id": "ORD-1234", "client_store_id": null, "TrackingDettaglio": [ { "Data": "01/10/2026 08:12", "Stato": "In transito", "Luogo": "Roma" } ], "statusCode": 3, "contractCode": "poste-express", "domain": "demo.spedisci.online" }

Eventi disponibili

EventoDescrizione
tracking.updateLo stato della spedizione cambia
shipment.createdUna spedizione viene creata
stock.openedIl corriere apre una giacenza sulla spedizione
stock.closedLa giacenza si chiude

Gli eventi si scelgono alla registrazione dell’endpoint, in Impostazioni → Webhooks oppure dalle API .

Risposta richiesta

Il vostro endpoint deve rispondere con:

  • 2xx se il webhook è stato ricevuto
  • 401 Unauthorized se la firma non è valida

In caso di errore di rete o di risposta 5xx l’invio viene ritentato 3 volte (dopo 30 secondi, 2 minuti e 5 minuti). Una risposta 4xx non viene ritentata. Un endpoint che non risponde 2xx per 72 ore viene disattivato automaticamente, con un avviso via e-mail.

Last updated on