The Vulnerability of Public Endpoints
When you expose a PHP script as a webhook receiver, you are effectively creating a public entry point into your application logic. Whether it is processing a payment from Stripe or syncing data from GitHub, a simple POST request is all it takes to trigger your code. The problem? Without verification, anyone who discovers the URL can spoof a request and trick your system into processing fake data.
Relying on IP whitelisting is brittle and hard to maintain. The industry standard for securing these connections is HMAC (Hash-based Message Authentication Code) signatures. This method allows you to verify both the identity of the sender and the integrity of the data using a shared secret.
How HMAC Verification Works
The process is straightforward but must be executed precisely. The sender takes the raw request body, hashes it using a secret key, and sends that hash in a header. Your PHP script performs the same calculation. If your generated hash matches the one in the header, the request is authentic.
<?php
function verify_webhook_signature() {
// 1. Retrieve the signature sent by the provider
$headerSignature = $_SERVER['HTTP_X_SIGNATURE_256'] ?? null;
if (!$headerSignature) {
header('HTTP/1.1 401 Unauthorized');
exit('Missing signature');
}
// 2. Get the raw payload (not $_POST, which is already parsed)
$payload = file_get_contents('php://input');
// 3. Your shared secret (store this in .env, never hardcode)
$secret = 'sk_live_51Mpw92837465';
// 4. Calculate the expected hash
$expectedSignature = hash_hmac('sha256', $payload, $secret);
// 5. Compare using a timing-attack safe function
if (!hash_equals($expectedSignature, $headerSignature)) {
header('HTTP/1.1 403 Forbidden');
exit('Invalid signature');
}
return true;
}
// Usage
if (verify_webhook_signature()) {
$data = json_decode(file_get_contents('php://input'), true);
// Proceed with business logic
echo "Success: Data verified.";
}
The Importance of hash_equals()
In the example above, we use hash_equals() rather than a standard == or === operator. This is a critical security step. Standard string comparisons return false as soon as a mismatch is found, meaning they take less time for incorrect inputs. This tiny difference in execution time can be measured by an attacker to guess your secret character by character. hash_equals() always takes the same amount of time regardless of the input, neutralizing timing attacks.
Reading the Raw Input
A common mistake in PHP is attempting to use the $_POST superglobal for signature verification. Most webhook providers send data as a JSON string in the request body. Once PHP parses that into $_POST, the formatting might change slightly, causing the hash to fail. Always use file_get_contents('php://input') to get the exact, byte-for-byte payload that was used to generate the original signature.
Practical Implementation Tips
- Log Failures: Log failed attempts with the payload and headers to debug configuration issues, but never log your actual secret key.
- Graceful Responses: If verification fails, return a 403 Forbidden status code. This tells the sender the request was received but rejected.
- Secret Rotation: Build your system so you can update the secret key without redeploying code, such as using environment variables.


