mirror of
https://github.com/pacnpal/django-anymail.git
synced 2025-12-20 03:41:05 -05:00
Minimum supported versions are now Django 2.0, Python 3.5. This touches a lot of code, to: * Remove obsolete portability code and workarounds (six, backports of email parsers, test utils, etc.) * Use Python 3 syntax (class defs, raise ... from, etc.) * Correct inheritance for mixin classes * Fix outdated docs content and links * Suppress Python 3 "unclosed SSLSocket" ResourceWarnings that are beyond our control (in integration tests due to boto3, python-sparkpost)
139 lines
5.1 KiB
ReStructuredText
139 lines
5.1 KiB
ReStructuredText
.. _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.
|