Skip to content

Commit 1a6086f

Browse files
committed
Security: rename WEBHOOK_AUTHORIZATION --> WEBHOOK_SECRET
This fixes a low severity security issue affecting Anymail v0.2--v1.3. Django error reporting includes the value of your Anymail WEBHOOK_AUTHORIZATION setting. In a properly-configured deployment, this should not be cause for concern. But if you have somehow exposed your Django error reports (e.g., by mis-deploying with DEBUG=True or by sending error reports through insecure channels), anyone who gains access to those reports could discover your webhook shared secret. An attacker could use this to post fabricated or malicious Anymail tracking/inbound events to your app, if you are using those Anymail features. The fix renames Anymail's webhook shared secret setting so that Django's error reporting mechanism will [sanitize][0] it. If you are using Anymail's event tracking and/or inbound webhooks, you should upgrade to this release and change "WEBHOOK_AUTHORIZATION" to "WEBHOOK_SECRET" in the ANYMAIL section of your settings.py. You may also want to [rotate the shared secret][1] value, particularly if you have ever exposed your Django error reports to untrusted individuals. If you are only using Anymail's EmailBackends for sending email and have not set up Anymail's webhooks, this issue does not affect you. The old WEBHOOK_AUTHORIZATION setting is still allowed in this release, but will issue a system-check warning when running most Django management commands. It will be removed completely in a near-future release, as a breaking change. Thanks to Charlie DeTar (@yourcelf) for responsibly reporting this security issue through private channels. [0]: https://docs.djangoproject.com/en/stable/ref/settings/#debug [1]: https://anymail.readthedocs.io/en/1.4/tips/securing_webhooks/#use-a-shared-authorization-secret
1 parent 4d34a18 commit 1a6086f

File tree

14 files changed

+99
-29
lines changed

14 files changed

+99
-29
lines changed

anymail/apps.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
from django.apps import AppConfig
2+
from django.core import checks
3+
4+
from .checks import check_deprecated_settings
25

36

47
class AnymailBaseConfig(AppConfig):
58
name = 'anymail'
69
verbose_name = "Anymail"
710

811
def ready(self):
9-
pass
12+
checks.register(check_deprecated_settings)

anymail/checks.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from django.conf import settings
2+
from django.core import checks
3+
4+
5+
def check_deprecated_settings(app_configs, **kwargs):
6+
errors = []
7+
8+
anymail_settings = getattr(settings, "ANYMAIL", {})
9+
10+
# anymail.W001: rename WEBHOOK_AUTHORIZATION to WEBHOOK_SECRET
11+
if "WEBHOOK_AUTHORIZATION" in anymail_settings:
12+
errors.append(checks.Warning(
13+
"The ANYMAIL setting 'WEBHOOK_AUTHORIZATION' has been renamed 'WEBHOOK_SECRET' to improve security.",
14+
hint="You must update your settings.py. The old name will stop working in a near-future release.",
15+
id="anymail.W001",
16+
))
17+
if hasattr(settings, "ANYMAIL_WEBHOOK_AUTHORIZATION"):
18+
errors.append(checks.Warning(
19+
"The ANYMAIL_WEBHOOK_AUTHORIZATION setting has been renamed ANYMAIL_WEBHOOK_SECRET to improve security.",
20+
hint="You must update your settings.py. The old name will stop working in a near-future release.",
21+
id="anymail.W001",
22+
))
23+
24+
return errors

anymail/webhooks/base.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,19 @@ class AnymailBasicAuthMixin(object):
2424
basic_auth = None # (Declaring class attr allows override by kwargs in View.as_view.)
2525

2626
def __init__(self, **kwargs):
27-
self.basic_auth = get_anymail_setting('webhook_authorization', default=[],
27+
self.basic_auth = get_anymail_setting('webhook_secret', default=[],
2828
kwargs=kwargs) # no esp_name -- auth is shared between ESPs
29+
if not self.basic_auth:
30+
# Temporarily allow deprecated WEBHOOK_AUTHORIZATION setting
31+
self.basic_auth = get_anymail_setting('webhook_authorization', default=[], kwargs=kwargs)
32+
2933
# Allow a single string:
3034
if isinstance(self.basic_auth, six.string_types):
3135
self.basic_auth = [self.basic_auth]
3236
if self.warn_if_no_basic_auth and len(self.basic_auth) < 1:
3337
warnings.warn(
3438
"Your Anymail webhooks are insecure and open to anyone on the web. "
35-
"You should set WEBHOOK_AUTHORIZATION in your ANYMAIL settings. "
39+
"You should set WEBHOOK_SECRET in your ANYMAIL settings. "
3640
"See 'Securing webhooks' in the Anymail docs.",
3741
AnymailInsecureWebhookWarning)
3842
# noinspection PyArgumentList

docs/esps/mailgun.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ for all events you want to receive:
197197

198198
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/mailgun/tracking/`
199199

200-
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
200+
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
201201
* *yoursite.example.com* is your Django site
202202

203203
If you use multiple Mailgun sending domains, you'll need to enter the webhook
@@ -232,7 +232,7 @@ The *action* for your route will be either:
232232
:samp:`forward("https://{random}:{random}@{yoursite.example.com}/anymail/mailgun/inbound/")`
233233
:samp:`forward("https://{random}:{random}@{yoursite.example.com}/anymail/mailgun/inbound_mime/")`
234234

235-
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
235+
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
236236
* *yoursite.example.com* is your Django site
237237

238238
Anymail accepts either of Mailgun's "fully-parsed" (.../inbound/) and "raw MIME" (.../inbound_mime/)

docs/esps/mailjet.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -232,7 +232,7 @@ the url in your Mailjet account REST API settings under `Event tracking (trigger
232232

233233
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/mailjet/tracking/`
234234

235-
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
235+
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
236236
* *yoursite.example.com* is your Django site
237237

238238
Be sure to enter the URL in the Mailjet settings for all the event types you want to receive.
@@ -263,7 +263,7 @@ The parseroute Url parameter will be:
263263

264264
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/mailjet/inbound/`
265265

266-
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
266+
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
267267
* *yoursite.example.com* is your Django site
268268

269269
Once you've done Mailjet's "basic setup" to configure the Parse API webhook, you can skip

docs/esps/mandrill.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ requires deploying your Django project twice:
206206

207207
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/mandrill/`
208208

209-
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
209+
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
210210
* *yoursite.example.com* is your Django site
211211
* (Note: Unlike Anymail's other supported ESPs, the Mandrill webhook uses this
212212
single url for both tracking and inbound events.)

docs/esps/postmark.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,7 @@ want to receive all these types of events):
181181

182182
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/postmark/tracking/`
183183

184-
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
184+
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
185185
* *yoursite.example.com* is your Django site
186186

187187
Anymail doesn't care about the "include bounce content" and "post only on first open"
@@ -216,7 +216,7 @@ The InboundHookUrl setting will be:
216216

217217
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/postmark/inbound/`
218218

219-
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
219+
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
220220
* *yoursite.example.com* is your Django site
221221

222222
Anymail handles the "parse an email" part of Postmark's instructions for you, but you'll

docs/esps/sendgrid.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,7 @@ the url in your `SendGrid mail settings`_, under "Event Notification":
284284

285285
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/sendgrid/tracking/`
286286

287-
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
287+
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
288288
* *yoursite.example.com* is your Django site
289289

290290
Be sure to check the boxes in the SendGrid settings for the event types you want to receive.
@@ -315,7 +315,7 @@ The Destination URL setting will be:
315315

316316
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/sendgrid/inbound/`
317317

318-
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
318+
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
319319
* *yoursite.example.com* is your Django site
320320

321321
Be sure the URL has a trailing slash. (SendGrid's inbound processing won't follow Django's

docs/esps/sparkpost.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ webhook in your `SparkPost account settings under "Webhooks"`_:
197197

198198
* Target URL: :samp:`https://{yoursite.example.com}/anymail/sparkpost/tracking/`
199199
* Authentication: choose "Basic Auth." For username and password enter the two halves of the
200-
*random:random* shared secret you created for your :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION`
200+
*random:random* shared secret you created for your :setting:`ANYMAIL_WEBHOOK_SECRET`
201201
Django setting. (Anymail doesn't support OAuth webhook auth.)
202202
* Events: click "Select" and then *clear* the checkbox for "Relay Events" category (which is for
203203
inbound email). You can leave all the other categories of events checked, or disable
@@ -235,7 +235,7 @@ The target parameter for the Relay Webhook will be:
235235

236236
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/sparkpost/inbound/`
237237

238-
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
238+
* *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
239239
* *yoursite.example.com* is your Django site
240240

241241
.. _Enabling Inbound Email Relaying:

docs/installation.rst

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -98,22 +98,22 @@ Skip this section if you won't be using Anymail's webhooks.
9898
or subject your app to malicious input data.
9999

100100
At a minimum, your site should **use https** and you should
101-
configure **webhook authorization** as described below.
101+
configure a **webhook secret** as described below.
102102

103103
See :ref:`securing-webhooks` for additional information.
104104

105105

106106
If you want to use Anymail's inbound or tracking webhooks:
107107

108108
1. In your :file:`settings.py`, add
109-
:setting:`WEBHOOK_AUTHORIZATION <ANYMAIL_WEBHOOK_AUTHORIZATION>`
109+
:setting:`WEBHOOK_SECRET <ANYMAIL_WEBHOOK_SECRET>`
110110
to the ``ANYMAIL`` block:
111111

112112
.. code-block:: python
113113
114114
ANYMAIL = {
115115
...
116-
'WEBHOOK_AUTHORIZATION': '<a random string>:<another random string>',
116+
'WEBHOOK_SECRET': '<a random string>:<another random string>',
117117
}
118118
119119
This setting should be a string with two sequences of random characters,
@@ -133,7 +133,7 @@ If you want to use Anymail's inbound or tracking webhooks:
133133
134134
(This setting is actually an HTTP basic auth string. You can also set it
135135
to a list of auth strings, to simplify credential rotation or use different auth
136-
with different ESPs. See :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` in the
136+
with different ESPs. See :setting:`ANYMAIL_WEBHOOK_SECRET` in the
137137
:ref:`securing-webhooks` docs for more details.)
138138

139139

@@ -160,7 +160,7 @@ If you want to use Anymail's inbound or tracking webhooks:
160160
:samp:`https://{random}:{random}@{yoursite.example.com}/anymail/{esp}/{type}/`
161161

162162
* "https" (rather than http) is *strongly recommended*
163-
* *random:random* is the WEBHOOK_AUTHORIZATION string you created in step 1
163+
* *random:random* is the WEBHOOK_SECRET string you created in step 1
164164
* *yoursite.example.com* is your Django site
165165
* "anymail" is the url prefix (from step 2)
166166
* *esp* is the lowercase name of your ESP (e.g., "sendgrid" or "mailgun")
@@ -266,20 +266,26 @@ Set to `True` to ignore these problems and send the email anyway. See
266266
:ref:`unsupported-features`. (Default `False`.)
267267

268268

269-
.. rubric:: WEBHOOK_AUTHORIZATION
269+
.. rubric:: WEBHOOK_SECRET
270270

271271
A `'random:random'` shared secret string. Anymail will reject incoming webhook calls
272272
from your ESP that don't include this authorization. You can also give a list of
273273
shared secret strings, and Anymail will allow ESP webhook calls that match any of them
274274
(to facilitate credential rotation). See :ref:`securing-webhooks`.
275275

276276
Default is unset, which leaves your webhooks insecure. Anymail
277-
will warn if you try to use webhooks with setting up authorization.
277+
will warn if you try to use webhooks without a shared secret.
278278

279279
This is actually implemented using HTTP basic authorization, and the string is
280280
technically a "username:password" format. But you should *not* use any real
281281
username or password for this shared secret.
282282

283+
.. versionchanged:: 1.4
284+
285+
The earlier WEBHOOK_AUTHORIZATION setting was renamed WEBHOOK_SECRET, so that
286+
Django error reporting sanitizes it. The old name is still allowed in v1.4,
287+
but will be removed in a near-future release. You should update your settings.
288+
283289

284290
.. setting:: ANYMAIL_REQUESTS_TIMEOUT
285291

0 commit comments

Comments
 (0)