How to Secure a Webhooks API

Protect your platform's data while delivering it to integrators via a Webhooks API

Secure your Webhooks API. Secure your Data. 

Industry best practices to implement a secure Webhooks API.

Building a Webhooks API is hard. Building a secure Webhooks API to protect your platform's data takes a large effort to do properly. This article will give you a rundown of industry best practices to implement security for your Webhooks API. EasyWebhooks simplifies webhook security for your platform with our hosted Webhooks API provider platform.

 

Table of Contents

Glossary


Before we get started, let's define some common terms that will be used throughout this article.

Webhook or Webhook Request

Going from everyone's favorite source, Wikipedia

Webhooks are "user-defined HTTP callbacks". They are usually triggered by some event, such as pushing code to a repository or a comment being posted to a blog. When that event occurs, the source site makes an HTTP request to the URL configured for the webhook. Users can configure them to cause events on one site to invoke behavior on another.

For the purposes of this article, we assume that a Webhook is an HTTP POST.

Webhooks API

  • An API that a platform creates to send state or events to integrators.
  • General a subscription must be made to specify the data the integrator wants to receive, and what type of event triggers sending the data. An example of this is a subscription to receive an event every time a user is created.

Webhook Provider or Sender

  • The platform that controls the data, events, and offers the Webhooks API.
  • The one making the Webhook request that gets sent to the Webhook Consumer.

Webhooks Consumer or Subscriber

  • The party that creates subscriptions to and listens for events that come from the Webhook Provider
  • Needs to create a web service that listens for events

Great! Now that we've defined some basic terms, let's start digging into the technical side of securing a Webhooks API.

What is the most secure protocol to use to send webhooks?


The first step on the journey to securing a Webhooks API is deciding what internet protocol you want to use when sending data. Here are the options available to you.

 

Plain HTTP

Wait, I thought this was about security? Well, in the simplest case if you don't care (though why are you reading this then?) just send a simple HTTP request. Interested parties could potentially intercept the contents of the messages, or tamper with their contents.

WARNING: Do not use plain HTTP for webhooks if you value data security

 

HTTPS SSL/TLS

Now we're getting somewhere. Require that your downstream consumers use HTTPS to provide end to end encryption. Attackers should be unable to read the payloads. There's really no excuse for not using HTTPS today, especially if you care about securing your Webhooks API and protecting your data. It's easy to get set up with SSL using a certificate authority like Let's Encrypt.

How does a webhook consumer verify the identity of the provider?


Now that your webhook consumers are securely using HTTPS, how can they be confident that requests are coming from your servers? It's possible that an attacker that knows their webhook url could send them fake data, or even attempt to overwhelm their systems. This could end up causing some major headaches for your webhook consumers. Fortunately, there are a few great ways to verify your identity.

 

Provide Whitelisted IPs and Domains

Let your webhook consumers know which IP addresses you will call them from. That way they can whitelist them and prevent bad traffic from coming in at the network layer. Stripe does this well by publishing a list of their IPs in txt and json files.

 

Token based verification in the Header

One of the most common methodologies of securing webhooks is to offer token based security, so as a webhook provider you would generate a shared secret that the consumer uses to validate every webhook you send them.

Many security minded webhook providers these days opt to provide a custom header that has a signature generated using the content of the request along with the shared secret. When the consumer receives the request, they apply the same algorithm that the provider does with the content and the secret they have on their end. If the signature they generate matches the one provided in the header, then they accept the request. Otherwise they reject it. Combined with HTTPS and whitelisting, the webhook consumer can be even more confident that the data they are receiving comes from you and not someone else.

An easy way to implement this is to use a base64 encoded HMAC SHA-256 hash. An example in python of how a consumer would check the request:

import base64
from hashlib import sha256
import hmac
import json

SHARED_SECRET = '20d990b2-487a-40f0-b8a0-3023f1d7b230'
request = {
'headers': {
'X-EasyWebhook-Signature': 'ToYfGphjG/9xKLBDVpxac1hlVX44XEIlXh+9aGNTTnk='
},
'body': '{"provider": "easywebhooks", "usefulness": "10/10"}'
}

payload = {
'provider': 'easywebhooks',
'usefulness': '10/10'
}

hmac_hash = hmac.new(SHARED_SECRET.encode("utf-8"), request.get('body').encode("utf-8"), digestmod=sha256)
expected_signature = base64.b64encode(hmac_hash.digest()).decode()
header_signature = request.get('headers').get('X-EasyWebhook-Signature')

if expected_signature != header_signature:
raise Exception('Warning: the webhook signatures do not match!')
print('The webhook signatures match!')

The provider only needs to generate the hash and put it in their header. If you stop here, and combine signature verification with both HTTPS and IP Whitelisting, you will make your webhooks extremely secure. There are still some weaknesses with signature verification, so if you want to protect against those then read on.

Timestamping to prevent Replay Attacks

There are certain time based attacks that a webhook provider using signature validation still exposes to their consumers, namely replay attacks, sometimes known as playback attacks. The gist is that an attacker intercepts a payload, and then replays it later to the same endpoint. Depending on what the receiver does, this could create duplicate entries in a database, create incorrect aggregates, or trigger unwanted behavior.

One way to prevent these attacks is to include a timestamp associated with the event, and include the timestamps as an input to the signature hash. If the attacker changes the timestamp to a recent time, then the hash recalculation will fail. Stripe, yet again, has this level of protection in their webhooks, and does a great job explaining it: .

A good rule of thumb is to allow a 5 minute window between the sender and the receiver because clock drift can cause the clocks on servers to get out of sync. This does mean that if an attacker can replay the attack within 5 minutes that it will nullify the advantage, so there is a tradeoff between security and false negatives.

With timestamping, our example above becomes this:

import base64
from hashlib import sha256
import hmac
import json
import sys
import time

from hashlib import sha256

SHARED_SECRET = '20d990b2-487a-40f0-b8a0-3023f1d7b230'
request = {
'headers': {
'X-EasyWebhook-Signature': 'wioQBPJSAkAu3o8dhiAVr1CwFYlWMaPnc4iAe5qxmJ4=',
'X-EasyWebhook-Timestamp': 1573426800842 ## update to a newer timestamp to test
},
'body': '{"provider": "easywebhooks", "usefulness": "10/10"}'
}

payload = {
'provider': 'easywebhooks',
'usefulness': '10/10'
}
hmac_hash = hmac.new(SHARED_SECRET.encode("utf-8"), request.get('body').encode("utf-8"), digestmod=sha256)

# update hash with the timestamp
header_timestamp = request.get('headers').get('X-EasyWebhook-Timestamp')
timestamp_bytes = header_timestamp.to_bytes(32, sys.byteorder)
hmac_hash.update(timestamp_bytes)

expected_signature = base64.b64encode(hmac_hash.digest()).decode()
header_signature = request.get('headers').get('X-EasyWebhook-Signature')

if expected_signature != header_signature:
raise Exception('Warning: the webhook signatures do not match!')

current_time_epoch_millis = int(time.time()*1000.0)
millis_in_five_minutes = 1000 * 60 * 5
if current_time_epoch_millis - header_timestamp > millis_in_five_minutes:
raise Exception('Warning: This request is too old. Potential replay attack.')

print('The webhook signatures match!')

 

How do you know you are sending your webhooks to the right consumer?


Verify the webhook consumer is who they say they are

Even with an airtight token verification system, there's still one major security hole: the webhook consumer. There's nothing preventing the consumer from skipping running the verification code, which might be okay to them, but if you value the security and privacy of your platform's data (and you should), you want to do whatever you can to make sure your counterparts are pulling their weight. 

 

Challenge-Response Checks

A great way to accomplish this is to send periodic security checks to your webhook consumers. The security check generally involves a GET request from the webhook provider that contains a special token parameter. The webhook consumer is supposed to take that token, and provide a response with an encrypted body based on the token provided, and the shared secret that it used to verify webhook headers. These are sometimes called Challenge-Response Checks. Similar to the version header, typically it is best to use a base64 encoded HMAC SHA-256 hash in the response. The code the provider needs is very similar to what the consumer needs to validate the header validation signature.

An example request:

$ curl -XGET https://consumer-url.com/target?challenge_token=abc123

{
"response_token": "9ffl20ff0q5qfjFjf39f="
}

Twitter does a great job of this for their webhooks. Twitter requires this webhook consumers to successfully response every time they create or update a webhook subscription for data from twitter, and also send periodic checks to ensure that the consumer is still functioning as expected, and has control of their server.

 

From their docs:

A CRC will be sent when you register your webhook URL, so implementing your CRC response code is a fundamental first step. Once your webhook is established, Twitter will trigger a CRC roughly every 24 hours from the last time we received a successful response. Your app can also trigger a CRC when needed by making a PUT request with your webhook id. Triggering a CRC is useful as you develop your webhook application, after deploying new code and restarting your service.

As you can see, they take extra care to ensure that their webhook consumers are who they say they are.

Conclusion


If you've made it down here, congratulations! You're now equipped to make your Webhooks API as secure as possible to protect your customer's data, while making it easy for integrators to build on top of your system. If you'd like to learn more about how to build a secure webhooks system, or would rather let the experts handle this, then please drop us a line, and we'll be happy to help!

Ready to secure your Webhooks API?


Fill out the form below to learn more about how EasyWebhooks can help you build a secure and scalable Webhooks API.