Do you ever think about how you push code on GitHub and it automatically updates on your deployed server?
Like, you just pushed. Nobody manually pulled the code. Nobody SSHed into the server. It just updated. How does that even work?
The mechanics behind this are called webhook signatures. And to understand it, I have an example. Webhook signature is like human trust. If two people have a deep level of trust and one of them sends any signal the other one follows it. No questions asked. Because the trust is already established between them.
Webhooks work the same way.
The Full Flow What Actually Happens
When you push code to GitHub, here's exactly what happens behind the scenes.
GitHub creates a signature using two things, the payload (the data about your push) and a secret key. That secret key is added by you in the GitHub webhook settings. GitHub generates the signature, and sends both the signature and the payload to your server's webhook endpoint.
Your server receives them. But your server doesn't just trust what GitHub sent. It creates its own signature using the same secret key and the same payload. Then it compares both signatures.
If they match the request is authentic. Server runs the pull, code gets updated.
If they don't match the request is rejected.
That's HMAC. Hash-based Message Authentication Code. The entire trust system between GitHub and your server built on a shared secret that only both of them know.
But Why Do We Even Need This
This is the part most people skip. They understand what happens but not why it exists.
Your server's webhook endpoint is publicly available. Anyone on the internet can send a POST request to it. Without verification, your server has no way to know who actually sent that request. Was it GitHub? Was it someone else?
Without webhook signature anyone could send a fake request to your webhook endpoint and trigger a code update. Or worse, modify your data. Your server would have no way to tell the difference.
The second reason is resources. Without webhooks, the alternative is setting up an active listener that constantly polls GitHub to check if anything changed. That's not reliable. That wastes resources. Webhooks solve this GitHub tells you when something happens, and you verify that it's actually GitHub telling you.
But There's a Catch Replay Attacks
If someone intercepts the request and copies the exact same payload and signature they can resend it. The signature would still match because the payload is the same.
To prevent this, a timestamp is added into the payload as a uniqueness factor. This timestamp is not just sitting in the raw payload GitHub includes it in the signature calculation. Your server adds the same timestamp on its side when recreating the signature.
So even if someone copies and replays the same request after a few minutes, the timestamp won't match the expected window. Request rejected.
Second Problem Timing Attacks
There's another problem that happens during signature comparison.
When your server compares two signatures using a normal comparison, it stops the moment it finds the first difference. That means if the first character doesn't match, it returns faster than if the first 10 characters match. This timing difference leaks information.
An attacker can send thousands of requests and measure the response time difference. Gradually, by analyzing the timing, they can guess the correct signature character by character.
To prevent this, you should use timingSafeEqual(). This function compares the signatures byte by byte and always takes the exact same amount of time regardless of how many bytes match or don't match. No timing difference. No information leaked.
hear's the code
const crypto = require('crypto');
function verifyWebhookSignature(payload, receivedSignature, secret) {
const expectedSignature = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
const trusted = Buffer.from(expectedSignature, 'utf8');
const untrusted = Buffer.from(receivedSignature, 'utf8');
return crypto.timingSafeEqual(trusted, untrusted);
}
Same length buffers. Same time every comparison. Timing attack prevented.
What This Actually Is
This is not just a GitHub feature. This is how trust is established between any two systems on the internet that don't share a session or a login.
The shared secret is the trust. The HMAC is the proof of that trust. And timingSafeEqual makes sure nobody can reverse-engineer that trust by watching the clock.
One push. Automatic deployment. Verified. Secure. That's the mechanics behind it.
If I got something wrong or anything can be improved — please drop it in the comments. I'm still learning and I want to get this right.
Top comments (0)