A few weeks ago we talked about webhooks integration and briefly mentioned webhook security as one of the key points to consider in this context. I feel like today we can dive deeper into this topic.
Webhooks are generally more vulnerable to attacks and less protected from them than APIs because webhooks make use of a publicly available URL and apart from SSL – that is, IF you use the HTTPS connection (which you probably should) – there is not much built-in encryption in the whole ‘send message – receive message’ scenario.
Hence when using webhooks in your integration scenarios – or really, in any scenario – webhook security is an important topic to review. And while there is no “one-size-fits-all” strategy here, below are some of the methods that are considered pretty much standard nowadays.
Prevent tampering attacks on requests with HMAC
One of the most popular ways to ensure webhook security is using HMAC – or, hash-based message authentication codes. In this scenario, the publisher and the subscriber share the same secret to ensure that the message sent / received is genuine.
The way this mechanism works is, in simple terms, having the content of a webhook payload and the secret key that the parties agreed on strung together and then encrypted using a cryptographic hash function such as MD5 or SHA-1. Of course, both systems need to “agree” upfront on the algorithm, i.e. the hash function, they’ll both use for the message verification.
The signature that results from this encryption is included in the request of the sending application (the ‘publisher’), i.e. in the webhook header. Ideally, the signature should contain a timestamp, too (more on that later). The receiving application (the ‘subscriber’) would then attempt to re-create the signature by performing its own encryption of the body with the shared secret upon the receipt of the request (remember – both parties agreed on the hash function, so the receiving application will use the same algorithm). If the resulting signature matches the one from the publisher, the message is concluded as genuine.
The HMAC approach is one of the most popular ones to protect your webhooks against tampering attacks which occur when an attacker forges a request to the endpoint defined in the webhook to make it look like it came from the original publisher – say, Salesforce or NetSuite – to send falsified data. Since an application that is configured to get a payload would accept incoming HTTP(S) requests from practically anybody, merely relying on the SSL encryption is not a guarantee of the authenticity of the sending application. It only protects from a message interception through some third party.
SIDE NOTE: Of course, in theory, one could verify the authenticity of the sender and the receiver via HTTPS using both client and server certificates, but in practice, it can be quite challenging to set up and is hence rarely practiced.
HMAC ensures that both the sender and the receiver are verified to each other, since only the authentic publisher (in our example Salesforce or NetSuite) would know the secret with which the HMAC in question was generated. Even if the request has been intercepted and a malicious sender is trying to transfer falsified data to the subscriber, only the body of the request would be susceptible to tampering; the false sender would never be able to re-create the correct HMAC since the shared secret is not part of the actual request.
ANOTHER SIDE NOTE: To ensure that the subscriber receives a request that has definitely NOT been tampered with, you can go ahead and white-list a number of IP addresses from which the subscriber is supposed to receive a request. This approach is, however, rather inconvenient for the publisher (i.e. for the sending application) because they will be constrained in the number of IPs they can use themselves. Plus it’s not a bullet-proof solution as there is still a small chance that an attacker can get control of an IP, or at least part of it.
Use timestamps to protect the receiver against replay attacks
This being said, the HMAC approach will not protect against the so-called replay attacks. This is because in a replay attack, an attacker will intercept the request and re-send it in its entirety multiple times to the subscriber. Since the body and the signature have not been tampered with at all, the subscriber will accept this request just fine again and again.
You might think – the request will be simply sent several times instead of once, so what? If it doesn’t contain false data, what does it matter? Well, it depends. Maybe an invoice will be generated several times, which is harmless, I’d say. Or, maybe a charge will be issued once a customer record is created, resulting in multiple charges all sent to the attacker. And that is a less fun scenario.
To protect your webhooks against replay attacks, the standard practice is to include a timestamp in the signature, hashed together with the secret key and the request body. This approach offers a solid protection because if the signature is left in its entirety but the timestamp is too old, the receiving application can safely reject the request. And the timestamp cannot be manipulated without making the signature completely invalid.
Even in the case when the subscriber timed out for some reason and the publisher has to retry the event delivery, you can configure the latter to generate a new signature and a new timestamp for each new attempt (this is what Verkada for example does).
SIDE NOTE: Another solution to protect yourself from replay attacks would be to create a mechanism for storing the IDs of the webhook events that have already been processed be the receiving application. However, this would require an extra setup and maintenance of such events database, and can be quite tricky to implement. Also, including a timestamp with the signature just feels like a more elegant way to tackle this problem.
Implement mutual TLS to protect sender from malicious destinations
Just like webhooks can be tampered with to make it look like an event has been sent from a legitimate publisher, webhooks can be also intercepted and sent to a wrong destination – or several wrong destinations, for that matter.
You might already know what TLS, which stands for Transport Layer Security, is but let’s recap it real briefly. TLS is a cryptographic protocol which ensures a secure end-to-end channel between the client and the server to share data. In other words, TLS is basically the successor to SSL. The server sends a certificate to the client, which in turn validates it against the number of its approved and trusted root certificates. This way, the client (the receiving application) can validate the identity of the server (the sending application) exactly.
But you can already see that this is a one-way validation – the recipient validates the sender. But for webhooks, this is not enough because ideally, the sender will also need to make sure that it is about to send possibly sensitive data to the correct recipient.
Mutual TLS allows to perform this validation both ways. In this scenario, the server is configured in such a way that it not only sends a certificate to the client but also requests the client to respond with its own certificate, which the server then validates against its own list of trusted root certificates.
As a result, should webhooks be intercepted and sent over to a wrong destination, the authentication will fail because the actual receiving application (the client) will never get a request from the sender (the webhook provider) to send its own certificate back to it for completing the validation process.
SIDE NOTE: In order to implement mutual TLS, the server must be configured accordingly. However, the configuration steps vary depending on what server exactly you use. The DocuSign documentation covers on this page for example the configuration for Apache2, F5, and NGINX.
Implement authentication token or basic auth to verify sender
We’ve already mentioned above that, in case of request interception by an attacker, HMAC can ensure the identity of the sender, in addition to verifying the message. But what if there was no interception to begin with and the sender is the one sending malicious or corrupt requests to attack your application’s database?
The API endpoint where webhook sends its events too is supposed to be publicly available – this is kinda one of the prerequisites for webhook integration. This also means – I have already mentioned it somewhere above – that ANYone can send events / data to this endpoint. So, ideally, we would like to make sure for the provider of the API endpoint that it receives only legitimate payload.
For this purpose, you can add an authentication token (some call it also verification token) in a header of the webhook request. This token will be then verified by the receiving end and if it’s a match, the request is accepted for further processing.
Somewhat similar strategy is followed by the username / password approach, also known as Basic Authentication approach. In this scenario, the endpoint of the target URL is associated with a unique username and password combination. To authenticate the webhook, you would need to enter username and password for this endpoint upon the webhook setup. When the webhook payload is sent, the fields for these values are included in the header of your HTTP request, and the receiving application will verify if these are the right match.
Yet another way to verify the sender is for the receiving end to actually know the IP address of the webhook provider and maybe even go a step further and white-list it. But as I have already mentioned above, this approach will be quite inconvenient for the sender if for some reason it needs a certain level of freedom with IPs allocation.
SIDE NOTE: While having the advantage of being relatively simple to set up, the token approach provides limited security because the token itself is delivered in plain text. Should an attacker get a hold of the token, they will have it very easy to impersonate the anticipated sender and send a compromised webhook request. The username / password can also appear as plain text in a URL – it depends on the webhook provider – , so be aware of that and make sure that you’re at least using HTTPS to enable credentials encryption via SSL.
Deliver just the bare minimum with skinny payloads
All these methods covered above can provide a really good, solid protection to your webhooks and / or endpoints. But what if you’re dealing with REALLY sensitive data and you cannot allow even the slightest chance of security breach?
Then the so-called skinny payload might be just the right strategy. Technically, it’s not a method to secure a webhook or the end-to-end communication channel between the sender and the receiver. Rather, it is a workaround to protect the sensitive data as a whole.
Instead of sending via a webhook the entire event to the receiver’s API endpoint, you would merely let the receiving application know that there is an important update for an event it has been waiting for, and in order to obtain the entire event data, the receiver needs to make a proper, authenticated API call. The term “skinny payload” means basically that you reduce the payload information down to the bare minimum and let the webhook take up the role of a simple notification.
If webhooks are the topic you’re on right now, you might find this interesting:
Webhook integrations | How to use webhooks in your integration flows
How to secure webhooks – Let’s recap the options
When thinking about webhook security, there are several approaches and strategies to implement. You can:
- Verify the publisher / source (i.e. the sending application)
- Verify the subscriber / consumer (i.e. the receiving application)
- Verify the message itself
Implementing the authentication token or basic authentication will definitely help you with the first point on the list. For subscriber verification, you can make use of the mutual TLS. Last but not least – to verify the message itself and protect both the sender and the receiver, you can implement HMAC together with timestamps.
In fact, HMAC is really considered to be the preferred strategy. If you peruse the documentation on webhooks from various popular SaaS products such as DocuSign or Stripe, most of them mention that they recommend HMAC over any other method they provide.
But regardless of the option, here are some best practices to follow when working with webhooks:
- If you’re a webhook provider, prepare good documentation, libraries and code examples for other developers to help them secure a webhook as quickly and easily as possible
- I don’t think it really needs to be mentioned but still – no really sensitive information like passwords, bank account data or medical records should be sent using webhooks. If real-time, event-driven is your topic here, use rather a skinny payload
- Tokens, timestamps, secrets, etc. can be compromised no matter how secure you made them. In order to ensure proper security level, provide a way to re-generate these should they get exposed. This way, you can at least ensure that no subsequent requests are affected by the security breach.