.. _securing-webhooks:

Securing webhooks
=================

If not used carefully, webhooks can create security vulnerabilities
in your Django application.

At minimum, you should **use https** and a **shared authentication secret**
for your Anymail webhooks. (Really, for *any* webhooks.)


.. sidebar:: Does this really matter?

    Short answer: yes!

    Do you allow unauthorized access to your APIs? Would you want
    someone eavesdropping on API calls? Of course not. Well, a webhook
    is just another API.

    Think about the data your ESP sends and what your app does with it.
    If your webhooks aren't secured, an attacker could...

    * accumulate a list of your customers' email addresses
    * fake bounces and spam reports, so you block valid user emails
    * see the full contents of email from your users
    * convincingly forge incoming mail, tricking your app into publishing
      spam or acting on falsified commands
    * overwhelm your DB with garbage data (do you store tracking info?
      incoming attachments?)

    ... or worse. Why take a chance?


Use https
---------

For security, your Django site must use https. The webhook URLs you
give your ESP need to start with *https* (not *http*).

Without https, the data your ESP sends your webhooks is exposed in transit.
This can include your customers' email addresses, the contents of messages
you receive through your ESP, the shared secret used to authorize calls
to your webhooks (described in the next section), and other data you'd
probably like to keep private.

Configuring https is beyond the scope of Anymail, but there are many good
tutorials on the web. If you've previously dismissed https as too expensive
or too complicated, please take another look. Free https certificates are
available from `Let's Encrypt`_, and many hosting providers now offer easy
https configuration using Let's Encrypt or their own no-cost option.

If you aren't able to use https on your Django site, then you should
not set up your ESP's webhooks.

.. _Let's Encrypt: https://letsencrypt.org/


.. setting:: ANYMAIL_WEBHOOK_SECRET

Use a shared authentication secret
----------------------------------

A webhook is an ordinary URL---anyone can post anything to it.
To avoid receiving random (or malicious) data in your webhook,
you should use a shared random secret that your ESP can present
with webhook data, to prove the post is coming from your ESP.

Most ESPs recommend using HTTP basic authentication as this shared
secret. Anymail includes support for this, via the
:setting:`!ANYMAIL_WEBHOOK_SECRET` setting.
Basic usage is covered in the
:ref:`webhooks configuration <webhooks-configuration>` docs.

If something posts to your webhooks without the required shared
secret as basic auth in the HTTP *Authorization* header, Anymail will
raise an :exc:`AnymailWebhookValidationFailure` error, which is
a subclass of Django's :exc:`~django.core.exceptions.SuspiciousOperation`.
This will result in an HTTP 400 "bad request" response, without further processing
the data or calling your signal receiver function.

In addition to a single "random:random" string, you can give a list
of authentication strings. Anymail will permit webhook calls that match
any of the authentication strings:

   .. code-block:: python

      ANYMAIL = {
          ...
          'WEBHOOK_SECRET': [
              'abcdefghijklmnop:qrstuvwxyz0123456789',
              'ZYXWVUTSRQPONMLK:JIHGFEDCBA9876543210',
          ],
      }

This facilitates credential rotation: first, append a new authentication
string to the list, and deploy your Django site. Then, update the webhook
URLs at your ESP to use the new authentication. Finally, remove the old
(now unused) authentication string from the list and re-deploy.

.. warning::

    If your webhook URLs don't use https, this shared authentication
    secret won't stay secret, defeating its purpose.


Signed webhooks
---------------

Some ESPs implement webhook signing, which is another method of verifying
the webhook data came from your ESP. Anymail will verify these signatures
for ESPs that support them. See the docs for your
:ref:`specific ESP <supported-esps>` for more details and configuration
that may be required.

Even with signed webhooks, it doesn't hurt to also use a shared secret.


Additional steps
----------------

Webhooks aren't unique to Anymail or to ESPs. They're used for many
different types of inter-site communication, and you can find additional
recommendations for improving webhook security on the web.

For example, you might consider:

* Tracking :attr:`~anymail.signals.AnymailTrackingEvent.event_id`,
  to avoid accidental double-processing of the same events (or replay attacks)
* Checking the webhook's :attr:`~anymail.signals.AnymailTrackingEvent.timestamp`
  is reasonably close the current time
* Configuring your firewall to reject webhook calls that come from
  somewhere other than your ESP's documented IP addresses (if your ESP
  provides this information)
* Rate-limiting webhook calls in your web server or using something
  like :pypi:`django-ratelimit`

But you should start with using https and a random shared secret via HTTP auth.
