Webhook Signatures Vérifier les charges et arrêter les demandes falsifiées
Apprenez comment fonctionnent les signatures HMAC-SHA256 des webhooks, comment implémenter une vérification en temps constant en Python, Node.js et PHP, et les erreurs courantes qui provoquent des échecs silencieux en production.
Anyone can send a POST request to your webhook endpoint. Without signature verification, your server has no way to know whether that request actually came from Stripe, GitHub, or your own infrastructure — or from an attacker replaying a legitimate event.
Webhook signature verification solves this. It’s how payment processors, version control platforms, and e-commerce systems let you prove authenticity before acting on incoming data. This article walks through how HMAC-SHA256 signatures work, the step-by-step verification pattern, and the subtle mistakes that cause failures in production.
Why Unauthenticated Webhooks Are a Security Risk
A webhook endpoint is just an HTTP handler exposed to the internet. Without verification, any request that hits it gets processed. Two attacks stand out:
- Spoofing — an attacker crafts a fake payload that looks like a legitimate event (a payment succeeded, a subscription renewed) and triggers action on your end without any real transaction occurring.
- Replay attacks — a legitimate request is captured in transit and re-submitted later. If your endpoint is idempotent but unprotected, the same event fires multiple times.
Both attacks are prevented by a shared secret combined with a cryptographic signature. The sender signs the payload; you verify the signature before processing anything.
How HMAC-SHA256 Signatures Work
HMAC (Hash-based Message Authentication Code) takes two inputs: a secret key et un message. It runs them through a hash function — SHA-256 in most webhook implementations — and outputs a fixed-length signature.
The key property: the same secret + message always produce the same signature, and changing even a single byte in the message produces a completely different output. Anyone without the secret key cannot produce a valid signature, even if they can see the payload in full.
In practice, the flow looks like this:
- You register your webhook with a service (e.g. Stripe). The service gives you a secret de signature.
- When the service sends an event, it computes
HMAC-SHA256(secret, payload)and includes the result in a request header. - Your server receives the request, computes the same HMAC using your stored secret and the raw request body, then compares the two signatures.
- If they match, the request is authentic. If they don’t, reject it.
The Verification Pattern, Step by Step
Here’s a Python implementation that mirrors what every major service expects:
import hmac
import hashlib
def verify_signature(payload: bytes, secret: str, received_sig: str) -> bool:
expected = hmac.new(
key=secret.encode("utf-8"),
msg=payload,
digestmod=hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, received_sig)
Two things matter here beyond the HMAC call itself. First: payload est bytes, not a decoded string — you must pass the raw request body exactly as received from the network. Second: the comparison uses hmac.compare_digest au lieu de ==. This is deliberate.
Why Constant-Time Comparison Is Not Optional
String comparison in most languages short-circuits: it returns false the moment it finds a mismatched character. An attacker who can measure response time can exploit this — sending thousands of requests with varying signatures and using timing differences to guess the correct value one character at a time. This is a timing attack.
hmac.compare_digest in Python, hash_equals in PHP, and crypto.timingSafeEqual in Node.js all take the same amount of time regardless of where the strings differ. Use them every time you compare a signature — no exceptions.
Real-World Header Formats: Stripe, GitHub, Shopify
Each service has a slightly different header name and signature encoding. Here’s what to parse for the three most common integrations.
Stripe
Stripe sends a Stripe-Signature header with this format:
Stripe-Signature: t=1492774577,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a05bd539e6d5b9d2a2d2fbe
Le t field is the Unix timestamp of when the event was sent. The v1 value is the HMAC-SHA256 of timestamp + "." + raw_body. When implementing manually:
- Parse the header to extract
tetv1. - Compute
HMAC-SHA256(secret, t + "." + raw_body). - Compare with
v1using constant-time equality. - Reject if
tis more than 300 seconds (5 minutes) from the current time.
GitHub
GitHub uses the X-Hub-Signature-256 en-tête :
X-Hub-Signature-256: sha256=6ffbb59b2300aae63f272406069a9788598b770f698a48021f99b32f8de06bb3
Strip the sha256= prefix, then compare the remainder against your HMAC of the raw body. GitHub does not embed a timestamp in the signed content, so implement event ID deduplication separately if replay protection matters for your use case.
Shopify
Shopify uses X-Shopify-Hmac-Sha256 with the digest Base64-encoded rather than hex:
X-Shopify-Hmac-Sha256: b6LPiZidmXnJQf0Ff/p7MZQPIBPN9TqAqLMgAfn2YLQ=
Decode both your computed HMAC and the header value from Base64 to raw bytes, then compare with a constant-time function. Comparing the Base64 strings directly can introduce subtle encoding-normalization bugs.
The Most Common Mistake: Raw Body vs. Parsed JSON
This is where most webhook signature verification failures happen in production. When your framework auto-parses the request body into a dict or object before your handler runs, the raw bytes are gone. The HMAC was computed against those original bytes — not against whatever your JSON library produces when it re-encodes the parsed structure.
Even when the data is semantically identical, {"amount": 100} et {"amount":100} (no space after the colon) produce different HMAC values. Field ordering differences, Unicode normalization, and float precision can all silently break verification without any obvious error.
Mettez également à jour la configuration de la connexion à la base de données de votre application. Dans MySQL PDO : buffer the raw request body before any parsing happens and pass those bytes to your verification function. In Express.js, register the route with express.raw() middleware rather than express.json():
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const rawBody = req.body; // Buffer — not a parsed object
const sig = req.headers['stripe-signature'];
// Verify rawBody against sig before JSON.parse()
});
Dans Django, utilisez request.body (bytes). In Laravel, use $request->getContent(). The pattern is consistent: access the raw bytes directly from the request, never re-serialize a parsed representation.
Replay Protection with Timestamps
A valid signature only proves the payload wasn’t tampered with in transit. It doesn’t prevent an attacker from capturing a legitimate request and re-submitting it hours later — the signature will still verify correctly because nothing has changed.
Stripe addresses this by including a Unix timestamp in the signed payload and recommending a 5-minute tolerance window. After extracting t from the Stripe-Signature header, reject requests where the timestamp falls outside that window:
import time
def is_within_tolerance(timestamp: int, tolerance_seconds: int = 300) -> bool:
return abs(time.time() - timestamp) <= tolerance_seconds
# In your handler:
if not is_within_tolerance(stripe_timestamp):
return HttpResponse(status=403) # Reject stale request
For services like GitHub and Shopify that don't embed timestamps in the signature scheme, implement your own replay protection by storing processed event IDs (available in the payload) and rejecting any ID you've already handled. A short-lived cache or Redis set with a TTL matching your processing window works well.
Verify Signatures Without Writing Code
When debugging a webhook integration — or auditing a payload you've already received — the Webhook Signature Validator lets you paste a payload, secret, and received signature to check verification instantly, without spinning up a local environment.
To generate HMAC signatures for testing your endpoint with crafted payloads, use the Générateur HMAC. It produces hex and Base64 outputs for SHA-256, SHA-512, and several other digest algorithms — useful for building test cases that cover the formats used by each service.
Référence Rapide
- HMAC-SHA256 with a shared secret is the standard signature scheme across Stripe, GitHub, Shopify, and most other services.
- Always use constant-time comparison (
hmac.compare_digest,hash_equals,crypto.timingSafeEqual) — not==. - Pass the raw request body bytes to your HMAC function, never a re-serialized JSON object.
- Check the timestamp on services that include one; Stripe's tolerance window is 5 minutes.
- Deduplicate by event ID for services without timestamp-based replay protection.
- Header name and encoding (hex vs Base64) differ by service — always check the integration docs.
Installez nos extensions
Ajoutez des outils IO à votre navigateur préféré pour un accès instantané et une recherche plus rapide
恵 Le Tableau de Bord Est Arrivé !
Tableau de Bord est une façon amusante de suivre vos jeux, toutes les données sont stockées dans votre navigateur. D'autres fonctionnalités arrivent bientôt !
Outils essentiels
Tout voir Nouveautés
Tout voirMise à jour: Notre dernier outil a été ajouté le 6 juin 2026
