CVE-2018-1000089: Insertion of Sensitive Information into Log File (CWE-532)
value: Anymail django-anymail version version 0.2 through 1.3 contains a CWE-532, CWE-209 vulnerability in WEBHOOK_AUTHORIZATION setting value that can result in An attacker with access to error logs could fabricate email tracking events. This attack appear to be exploitable via If you have exposed your Django error reports, an attacker could discover your ANYMAIL_WEBHOOK setting and use this to post fabricated or malicious Anymail tracking/inbound events to your app. This vulnerability appears to have been fixed in v1.4.
CWE description: Information written to log files can be of a sensitive nature and give valuable guidance to an attacker or expose sensitive user information.
CVSS v3 base score: 7.4
Published: 2018-03-13T15:29Z
Repository: https://github.com/anymail/django-anymail
Language: Other
Files changed: anymail/apps.py, anymail/checks.py, anymail/webhooks/base.py, docs/esps/mailgun.rst, docs/esps/mailjet.rst, docs/esps/mandrill.rst, docs/esps/postmark.rst, docs/esps/sendgrid.rst, docs/esps/sparkpost.rst, docs/installation.rst, docs/tips/securing_webhooks.rst, tests/test_checks.py, tests/test_mandrill_webhooks.py, tests/webhook_cases.py
Commit date: 2018-02-08 11:38:15 -0800
Commit message: 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

Diff: diff --git a/anymail/apps.py b/anymail/apps.py
index ab19c6e..72a7408 100644
--- a/anymail/apps.py
+++ b/anymail/apps.py
@@ -1,9 +1,12 @@
 from django.apps import AppConfig
+from django.core import checks
+
+from .checks import check_deprecated_settings
 
 
 class AnymailBaseConfig(AppConfig):
     name = 'anymail'
     verbose_name = "Anymail"
 
     def ready(self):
-        pass
+        checks.register(check_deprecated_settings)
diff --git a/anymail/checks.py b/anymail/checks.py
new file mode 100644
index 0000000..3c54e3b
--- /dev/null
+++ b/anymail/checks.py
@@ -0,0 +1,24 @@
+from django.conf import settings
+from django.core import checks
+
+
+def check_deprecated_settings(app_configs, **kwargs):
+    errors = []
+
+    anymail_settings = getattr(settings, "ANYMAIL", {})
+
+    # anymail.W001: rename WEBHOOK_AUTHORIZATION to WEBHOOK_SECRET
+    if "WEBHOOK_AUTHORIZATION" in anymail_settings:
+        errors.append(checks.Warning(
+            "The ANYMAIL setting 'WEBHOOK_AUTHORIZATION' has been renamed 'WEBHOOK_SECRET' to improve security.",
+            hint="You must update your settings.py. The old name will stop working in a near-future release.",
+            id="anymail.W001",
+        ))
+    if hasattr(settings, "ANYMAIL_WEBHOOK_AUTHORIZATION"):
+        errors.append(checks.Warning(
+            "The ANYMAIL_WEBHOOK_AUTHORIZATION setting has been renamed ANYMAIL_WEBHOOK_SECRET to improve security.",
+            hint="You must update your settings.py. The old name will stop working in a near-future release.",
+            id="anymail.W001",
+        ))
+
+    return errors
diff --git a/anymail/webhooks/base.py b/anymail/webhooks/base.py
index 2bfd36e..30207e7 100644
--- a/anymail/webhooks/base.py
+++ b/anymail/webhooks/base.py
@@ -22,19 +22,23 @@ class AnymailBasicAuthMixin(object):
 
     # List of allowable HTTP basic-auth 'user:pass' strings.
     basic_auth = None  # (Declaring class attr allows override by kwargs in View.as_view.)
 
     def __init__(self, **kwargs):
-        self.basic_auth = get_anymail_setting('webhook_authorization', default=[],
+        self.basic_auth = get_anymail_setting('webhook_secret', default=[],
                                               kwargs=kwargs)  # no esp_name -- auth is shared between ESPs
+        if not self.basic_auth:
+            # Temporarily allow deprecated WEBHOOK_AUTHORIZATION setting
+            self.basic_auth = get_anymail_setting('webhook_authorization', default=[], kwargs=kwargs)
+
         # Allow a single string:
         if isinstance(self.basic_auth, six.string_types):
             self.basic_auth = [self.basic_auth]
         if self.warn_if_no_basic_auth and len(self.basic_auth) < 1:
             warnings.warn(
                 "Your Anymail webhooks are insecure and open to anyone on the web. "
-                "You should set WEBHOOK_AUTHORIZATION in your ANYMAIL settings. "
+                "You should set WEBHOOK_SECRET in your ANYMAIL settings. "
                 "See 'Securing webhooks' in the Anymail docs.",
                 AnymailInsecureWebhookWarning)
         # noinspection PyArgumentList
         super(AnymailBasicAuthMixin, self).__init__(**kwargs)
 
diff --git a/docs/esps/mailgun.rst b/docs/esps/mailgun.rst
index 3802e50..1ba3e4d 100644
--- a/docs/esps/mailgun.rst
+++ b/docs/esps/mailgun.rst
@@ -195,11 +195,11 @@ the url in your `Mailgun dashboard`_ on the "Webhooks" tab. Mailgun allows you t
 a different URL for each event type: just enter this same Anymail tracking URL
 for all events you want to receive:
 
    :samp:`https://{random}:{random}@{yoursite.example.com}/anymail/mailgun/tracking/`
 
-     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
+     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
      * *yoursite.example.com* is your Django site
 
 If you use multiple Mailgun sending domains, you'll need to enter the webhook
 URLs for each of them, using the selector on the left side of Mailgun's dashboard.
 
@@ -230,11 +230,11 @@ using Mailgun's API, or simply using the "Routes" tab in your `Mailgun dashboard
 The *action* for your route will be either:
 
    :samp:`forward("https://{random}:{random}@{yoursite.example.com}/anymail/mailgun/inbound/")`
    :samp:`forward("https://{random}:{random}@{yoursite.example.com}/anymail/mailgun/inbound_mime/")`
 
-     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
+     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
      * *yoursite.example.com* is your Django site
 
 Anymail accepts either of Mailgun's "fully-parsed" (.../inbound/) and "raw MIME" (.../inbound_mime/)
 formats; the URL tells Mailgun which you want. Because Anymail handles parsing and normalizing the data,
 both are equally easy to use. The raw MIME option will give the most accurate representation of *any*
diff --git a/docs/esps/mailjet.rst b/docs/esps/mailjet.rst
index 06b8784..1853a2b 100644
--- a/docs/esps/mailjet.rst
+++ b/docs/esps/mailjet.rst
@@ -230,11 +230,11 @@ Status tracking webhooks
 If you are using Anymail's normalized :ref:`status tracking <event-tracking>`, enter
 the url in your Mailjet account REST API settings under `Event tracking (triggers)`_:
 
    :samp:`https://{random}:{random}@{yoursite.example.com}/anymail/mailjet/tracking/`
 
-     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
+     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
      * *yoursite.example.com* is your Django site
 
 Be sure to enter the URL in the Mailjet settings for all the event types you want to receive.
 It's also recommended to select the "group events" checkbox for each trigger, to minimize your
 server load.
@@ -261,11 +261,11 @@ handling, follow Mailjet's `Parse API inbound emails`_ guide to set up Anymail's
 
 The parseroute Url parameter will be:
 
    :samp:`https://{random}:{random}@{yoursite.example.com}/anymail/mailjet/inbound/`
 
-     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
+     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
      * *yoursite.example.com* is your Django site
 
 Once you've done Mailjet's "basic setup" to configure the Parse API webhook, you can skip
 ahead to the "use your own domain" section of their guide. (Anymail normalizes the inbound
 event for you, so you won't need to worry about Mailjet's event and attachment formats.)
diff --git a/docs/esps/mandrill.rst b/docs/esps/mandrill.rst
index d3ac498..b074d62 100644
--- a/docs/esps/mandrill.rst
+++ b/docs/esps/mandrill.rst
@@ -204,11 +204,11 @@ requires deploying your Django project twice:
    events you want to receive) and/or `inbound route webhooks`_.
    In either case, the webhook url is:
 
       :samp:`https://{random}:{random}@{yoursite.example.com}/anymail/mandrill/`
 
-        * *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
+        * *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
         * *yoursite.example.com* is your Django site
         * (Note: Unlike Anymail's other supported ESPs, the Mandrill webhook uses this
           single url for both tracking and inbound events.)
 
 2. Mandrill will provide you a "webhook authentication key" once it verifies the URL
diff --git a/docs/esps/postmark.rst b/docs/esps/postmark.rst
index 7ec25d7..da0fe73 100644
--- a/docs/esps/postmark.rst
+++ b/docs/esps/postmark.rst
@@ -179,11 +179,11 @@ Settings > Outbound > Webhooks. You should enter this same Anymail tracking URL
 for all of the "Delivery webhook," "Bounce webhook," and "Opens webhook" (if you
 want to receive all these types of events):
 
    :samp:`https://{random}:{random}@{yoursite.example.com}/anymail/postmark/tracking/`
 
-     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
+     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
      * *yoursite.example.com* is your Django site
 
 Anymail doesn't care about the "include bounce content" and "post only on first open"
 Postmark webhook settings: whether to use them is your choice.
 
@@ -214,11 +214,11 @@ an inbound server pointing to Anymail's inbound webhook.
 
 The InboundHookUrl setting will be:
 
    :samp:`https://{random}:{random}@{yoursite.example.com}/anymail/postmark/inbound/`
 
-     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
+     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
      * *yoursite.example.com* is your Django site
 
 Anymail handles the "parse an email" part of Postmark's instructions for you, but you'll
 likely want to work through the other sections to set up a custom inbound domain, and
 perhaps configure inbound spam blocking.
diff --git a/docs/esps/sendgrid.rst b/docs/esps/sendgrid.rst
index 7a1708c..ddd6a2c 100644
--- a/docs/esps/sendgrid.rst
+++ b/docs/esps/sendgrid.rst
@@ -282,11 +282,11 @@ Status tracking webhooks
 If you are using Anymail's normalized :ref:`status tracking <event-tracking>`, enter
 the url in your `SendGrid mail settings`_, under "Event Notification":
 
    :samp:`https://{random}:{random}@{yoursite.example.com}/anymail/sendgrid/tracking/`
 
-     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
+     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
      * *yoursite.example.com* is your Django site
 
 Be sure to check the boxes in the SendGrid settings for the event types you want to receive.
 
 SendGrid will report these Anymail :attr:`~anymail.signals.AnymailTrackingEvent.event_type`\s:
@@ -313,11 +313,11 @@ Anymail's inbound webhook.
 
 The Destination URL setting will be:
 
    :samp:`https://{random}:{random}@{yoursite.example.com}/anymail/sendgrid/inbound/`
 
-     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
+     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
      * *yoursite.example.com* is your Django site
 
 Be sure the URL has a trailing slash. (SendGrid's inbound processing won't follow Django's
 :setting:`APPEND_SLASH` redirect.)
 
diff --git a/docs/esps/sparkpost.rst b/docs/esps/sparkpost.rst
index 47ad8ed..7913245 100644
--- a/docs/esps/sparkpost.rst
+++ b/docs/esps/sparkpost.rst
@@ -195,11 +195,11 @@ Status tracking webhooks
 If you are using Anymail's normalized :ref:`status tracking <event-tracking>`, set up the
 webhook in your `SparkPost account settings under "Webhooks"`_:
 
 * Target URL: :samp:`https://{yoursite.example.com}/anymail/sparkpost/tracking/`
 * Authentication: choose "Basic Auth." For username and password enter the two halves of the
-  *random:random* shared secret you created for your :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION`
+  *random:random* shared secret you created for your :setting:`ANYMAIL_WEBHOOK_SECRET`
   Django setting. (Anymail doesn't support OAuth webhook auth.)
 * Events: click "Select" and then *clear* the checkbox for "Relay Events" category (which is for
   inbound email). You can leave all the other categories of events checked, or disable
   any you aren't interested in tracking.
 
@@ -233,10 +233,10 @@ Anymail's inbound webhook.
 
 The target parameter for the Relay Webhook will be:
 
    :samp:`https://{random}:{random}@{yoursite.example.com}/anymail/sparkpost/inbound/`
 
-     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
+     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
      * *yoursite.example.com* is your Django site
 
 .. _Enabling Inbound Email Relaying:
    https://www.sparkpost.com/docs/tech-resources/inbound-email-relay-webhook/
diff --git a/docs/installation.rst b/docs/installation.rst
index fff5a18..803d3ff 100644
--- a/docs/installation.rst
+++ b/docs/installation.rst
@@ -96,26 +96,26 @@ Skip this section if you won't be using Anymail's webhooks.
     You must use care to **avoid creating security vulnerabilities**
     that could expose your users' emails and other private information,
     or subject your app to malicious input data.
 
     At a minimum, your site should **use https** and you should
-    configure **webhook authorization** as described below.
+    configure a **webhook secret** as described below.
 
     See :ref:`securing-webhooks` for additional information.
 
 
 If you want to use Anymail's inbound or tracking webhooks:
 
 1. In your :file:`settings.py`, add
-   :setting:`WEBHOOK_AUTHORIZATION <ANYMAIL_WEBHOOK_AUTHORIZATION>`
+   :setting:`WEBHOOK_SECRET <ANYMAIL_WEBHOOK_SECRET>`
    to the ``ANYMAIL`` block:
 
    .. code-block:: python
 
       ANYMAIL = {
           ...
-          'WEBHOOK_AUTHORIZATION': '<a random string>:<another random string>',
+          'WEBHOOK_SECRET': '<a random string>:<another random string>',
       }
 
    This setting should be a string with two sequences of random characters,
    separated by a colon. It is used as a shared secret, known only to your ESP
    and your Django app, to ensure nobody else can call your webhooks.
@@ -131,11 +131,11 @@ If you want to use Anymail's inbound or tracking webhooks:
 
       $ python -c "from django.utils import crypto; print(':'.join(crypto.get_random_string(16) for _ in range(2)))"
 
    (This setting is actually an HTTP basic auth string. You can also set it
    to a list of auth strings, to simplify credential rotation or use different auth
-   with different ESPs. See :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` in the
+   with different ESPs. See :setting:`ANYMAIL_WEBHOOK_SECRET` in the
    :ref:`securing-webhooks` docs for more details.)
 
 
 2. In your project's :file:`urls.py`, add routing for the Anymail webhook urls:
 
@@ -158,11 +158,11 @@ If you want to use Anymail's inbound or tracking webhooks:
    In most cases, the URL will be:
 
    :samp:`https://{random}:{random}@{yoursite.example.com}/anymail/{esp}/{type}/`
 
      * "https" (rather than http) is *strongly recommended*
-     * *random:random* is the WEBHOOK_AUTHORIZATION string you created in step 1
+     * *random:random* is the WEBHOOK_SECRET string you created in step 1
      * *yoursite.example.com* is your Django site
      * "anymail" is the url prefix (from step 2)
      * *esp* is the lowercase name of your ESP (e.g., "sendgrid" or "mailgun")
      * *type* is either "tracking" for Anymail's sent-mail event tracking webhooks,
        or "inbound" for receiving email
@@ -264,24 +264,30 @@ Whether Anymail should raise :exc:`~anymail.exceptions.AnymailUnsupportedFeature
 errors for email with features that can't be accurately communicated to the ESP.
 Set to `True` to ignore these problems and send the email anyway. See
 :ref:`unsupported-features`. (Default `False`.)
 
 
-.. rubric:: WEBHOOK_AUTHORIZATION
+.. rubric:: WEBHOOK_SECRET
 
 A `'random:random'` shared secret string. Anymail will reject incoming webhook calls
 from your ESP that don't include this authorization. You can also give a list of
 shared secret strings, and Anymail will allow ESP webhook calls that match any of them
 (to facilitate credential rotation). See :ref:`securing-webhooks`.
 
 Default is unset, which leaves your webhooks insecure. Anymail
-will warn if you try to use webhooks with setting up authorization.
+will warn if you try to use webhooks without a shared secret.
 
 This is actually implemented using HTTP basic authorization, and the string is
 technically a "username:password" format. But you should *not* use any real
 username or password for this shared secret.
 
+.. versionchanged:: 1.4
+
+    The earlier WEBHOOK_AUTHORIZATION setting was renamed WEBHOOK_SECRET, so that
+    Django error reporting sanitizes it. The old name is still allowed in v1.4,
+    but will be removed in a near-future release. You should update your settings.
+
 
 .. setting:: ANYMAIL_REQUESTS_TIMEOUT
 
 .. rubric:: REQUESTS_TIMEOUT
 
diff --git a/docs/tips/securing_webhooks.rst b/docs/tips/securing_webhooks.rst
index ac8399f..61e8329 100644
--- a/docs/tips/securing_webhooks.rst
+++ b/docs/tips/securing_webhooks.rst
@@ -27,11 +27,11 @@ tutorials on the web.
 
 If you aren't able to use https on your Django site, then you should
 not set up your ESP's webhooks.
 
 
-.. setting:: ANYMAIL_WEBHOOK_AUTHORIZATION
+.. setting:: ANYMAIL_WEBHOOK_SECRET
 
 Use a shared authorization secret
 ---------------------------------
 
 A webhook is an ordinary URL---anyone can post anything to it.
@@ -39,11 +39,11 @@ 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 authorization as this shared
 secret. Anymail includes support for this, via the
-:setting:`!ANYMAIL_WEBHOOK_AUTHORIZATION` setting.
+: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
@@ -58,11 +58,11 @@ any of the authorization strings:
 
    .. code-block:: python
 
       ANYMAIL = {
           ...
-          'WEBHOOK_AUTHORIZATION': [
+          'WEBHOOK_SECRET': [
               'abcdefghijklmnop:qrstuvwxyz0123456789',
               'ZYXWVUTSRQPONMLK:JIHGFEDCBA9876543210',
           ],
       }
 
diff --git a/tests/test_checks.py b/tests/test_checks.py
new file mode 100644
index 0000000..8d31534
--- /dev/null
+++ b/tests/test_checks.py
@@ -0,0 +1,27 @@
+from django.core import checks
+from django.test import SimpleTestCase
+from django.test.utils import override_settings
+
+from anymail.checks import check_deprecated_settings
+
+from .utils import AnymailTestMixin
+
+
+class DeprecatedSettingsTests(SimpleTestCase, AnymailTestMixin):
+    @override_settings(ANYMAIL={"WEBHOOK_AUTHORIZATION": "abcde:12345"})
+    def test_webhook_authorization(self):
+        errors = check_deprecated_settings(None)
+        self.assertEqual(errors, [checks.Warning(
+            "The ANYMAIL setting 'WEBHOOK_AUTHORIZATION' has been renamed 'WEBHOOK_SECRET' to improve security.",
+            hint="You must update your settings.py. The old name will stop working in a near-future release.",
+            id="anymail.W001",
+        )])
+
+    @override_settings(ANYMAIL_WEBHOOK_AUTHORIZATION="abcde:12345", ANYMAIL={})
+    def test_anymail_webhook_authorization(self):
+        errors = check_deprecated_settings(None)
+        self.assertEqual(errors, [checks.Warning(
+            "The ANYMAIL_WEBHOOK_AUTHORIZATION setting has been renamed ANYMAIL_WEBHOOK_SECRET to improve security.",
+            hint="You must update your settings.py. The old name will stop working in a near-future release.",
+            id="anymail.W001",
+        )])
diff --git a/tests/test_mandrill_webhooks.py b/tests/test_mandrill_webhooks.py
index 1fc19f3..0dd2a5f 100644
--- a/tests/test_mandrill_webhooks.py
+++ b/tests/test_mandrill_webhooks.py
@@ -85,11 +85,11 @@ class MandrillWebhookSecurityTestCase(WebhookTestCase, WebhookBasicAuthTestsMixi
     def test_verifies_bad_signature(self):
         kwargs = mandrill_args([{'event': 'send'}], key="wrong API key")
         response = self.client.post(**kwargs)
         self.assertEqual(response.status_code, 400)
 
-    @override_settings(ANYMAIL={})  # clear WEBHOOK_AUTHORIZATION from WebhookTestCase
+    @override_settings(ANYMAIL={})  # clear WEBHOOK_SECRET from WebhookTestCase
     def test_no_basic_auth(self):
         # Signature validation should work properly if you're not using basic auth
         self.clear_basic_auth()
         kwargs = mandrill_args([{'event': 'send'}], auth="")
         response = self.client.post(**kwargs)
@@ -97,11 +97,11 @@ class MandrillWebhookSecurityTestCase(WebhookTestCase, WebhookBasicAuthTestsMixi
 
     @override_settings(
         ALLOWED_HOSTS=['127.0.0.1', '.example.com'],
         ANYMAIL={
             "MANDRILL_WEBHOOK_URL": "https://abcde:12345@example.com/anymail/mandrill/",
-            "WEBHOOK_AUTHORIZATION": "abcde:12345",
+            "WEBHOOK_SECRET": "abcde:12345",
         })
     def test_webhook_url_setting(self):
         # If Django can't build_absolute_uri correctly (e.g., because your proxy
         # frontend isn't setting the proxy headers correctly), you must set
         # MANDRILL_WEBHOOK_URL to the actual public url where Mandrill calls the webhook.
@@ -109,11 +109,11 @@ class MandrillWebhookSecurityTestCase(WebhookTestCase, WebhookBasicAuthTestsMixi
         kwargs = mandrill_args([{'event': 'send'}], host="https://example.com/", auth="abcde:12345")
         response = self.client.post(SERVER_NAME="127.0.0.1", **kwargs)
         self.assertEqual(response.status_code, 200)
 
     # override WebhookBasicAuthTestsMixin version of this test
-    @override_settings(ANYMAIL={'WEBHOOK_AUTHORIZATION': ['cred1:pass1', 'cred2:pass2']})
+    @override_settings(ANYMAIL={'WEBHOOK_SECRET': ['cred1:pass1', 'cred2:pass2']})
     def test_supports_credential_rotation(self):
         """You can supply a list of basic auth credentials, and any is allowed"""
         self.set_basic_auth('cred1', 'pass1')
         response = self.client.post(**mandrill_args(auth="cred1:pass1"))
         self.assertEqual(response.status_code, 200)
diff --git a/tests/webhook_cases.py b/tests/webhook_cases.py
index 249bf19..0b8d0fb 100644
--- a/tests/webhook_cases.py
+++ b/tests/webhook_cases.py
@@ -12,11 +12,11 @@ from .utils import AnymailTestMixin, ClientWithCsrfChecks
 def event_handler(sender, event, esp_name, **kwargs):
     """Prototypical webhook signal handler"""
     pass
 
 
-@override_settings(ANYMAIL={'WEBHOOK_AUTHORIZATION': 'username:password'})
+@override_settings(ANYMAIL={'WEBHOOK_SECRET': 'username:password'})
 class WebhookTestCase(AnymailTestMixin, SimpleTestCase):
     """Base for testing webhooks
 
     - connects webhook signal handlers
     - sets up basic auth by default (since most ESP webhooks warn if it's not enabled)
@@ -109,11 +109,11 @@ class WebhookBasicAuthTestsMixin(object):
     def test_verifies_missing_auth(self):
         self.clear_basic_auth()
         response = self.call_webhook()
         self.assertEqual(response.status_code, 400)
 
-    @override_settings(ANYMAIL={'WEBHOOK_AUTHORIZATION': ['cred1:pass1', 'cred2:pass2']})
+    @override_settings(ANYMAIL={'WEBHOOK_SECRET': ['cred1:pass1', 'cred2:pass2']})
     def test_supports_credential_rotation(self):
         """You can supply a list of basic auth credentials, and any is allowed"""
         self.set_basic_auth('cred1', 'pass1')
         response = self.call_webhook()
         self.assertEqual(response.status_code, 200)
@@ -123,5 +123,11 @@ class WebhookBasicAuthTestsMixin(object):
         self.assertEqual(response.status_code, 200)
 
         self.set_basic_auth('baduser', 'wrongpassword')
         response = self.call_webhook()
         self.assertEqual(response.status_code, 400)
+
+    @override_settings(ANYMAIL={'WEBHOOK_AUTHORIZATION': "username:password"})
+    def test_deprecated_setting(self):
+        """The older WEBHOOK_AUTHORIZATION setting is still supported (for now)"""
+        response = self.call_webhook()
+        self.assertEqual(response.status_code, 200)

Vulnerable code:         pass
        self.basic_auth = get_anymail_setting('webhook_authorization', default=[],
                "You should set WEBHOOK_AUTHORIZATION in your ANYMAIL settings. "
     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
        * *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
  *random:random* shared secret you created for your :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION`
     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` shared secret
    configure **webhook authorization** as described below.
   :setting:`WEBHOOK_AUTHORIZATION <ANYMAIL_WEBHOOK_AUTHORIZATION>`
          'WEBHOOK_AUTHORIZATION': '<a random string>:<another random string>',
   with different ESPs. See :setting:`ANYMAIL_WEBHOOK_AUTHORIZATION` in the
     * *random:random* is the WEBHOOK_AUTHORIZATION string you created in step 1
.. rubric:: WEBHOOK_AUTHORIZATION
will warn if you try to use webhooks with setting up authorization.
.. setting:: ANYMAIL_WEBHOOK_AUTHORIZATION
:setting:`!ANYMAIL_WEBHOOK_AUTHORIZATION` setting.
          'WEBHOOK_AUTHORIZATION': [
    @override_settings(ANYMAIL={})  # clear WEBHOOK_AUTHORIZATION from WebhookTestCase
            "WEBHOOK_AUTHORIZATION": "abcde:12345",
    @override_settings(ANYMAIL={'WEBHOOK_AUTHORIZATION': ['cred1:pass1', 'cred2:pass2']})
@override_settings(ANYMAIL={'WEBHOOK_AUTHORIZATION': 'username:password'})
    @override_settings(ANYMAIL={'WEBHOOK_AUTHORIZATION': ['cred1:pass1', 'cred2:pass2']})
Fixed code: from django.core import checks

from .checks import check_deprecated_settings
        checks.register(check_deprecated_settings)
from django.conf import settings
from django.core import checks


def check_deprecated_settings(app_configs, **kwargs):
    errors = []

    anymail_settings = getattr(settings, "ANYMAIL", {})

    # anymail.W001: rename WEBHOOK_AUTHORIZATION to WEBHOOK_SECRET
    if "WEBHOOK_AUTHORIZATION" in anymail_settings:
        errors.append(checks.Warning(
            "The ANYMAIL setting 'WEBHOOK_AUTHORIZATION' has been renamed 'WEBHOOK_SECRET' to improve security.",
            hint="You must update your settings.py. The old name will stop working in a near-future release.",
            id="anymail.W001",
        ))
    if hasattr(settings, "ANYMAIL_WEBHOOK_AUTHORIZATION"):
        errors.append(checks.Warning(
            "The ANYMAIL_WEBHOOK_AUTHORIZATION setting has been renamed ANYMAIL_WEBHOOK_SECRET to improve security.",
            hint="You must update your settings.py. The old name will stop working in a near-future release.",
            id="anymail.W001",
        ))

    return errors
        self.basic_auth = get_anymail_setting('webhook_secret', default=[],
        if not self.basic_auth:
            # Temporarily allow deprecated WEBHOOK_AUTHORIZATION setting
            self.basic_auth = get_anymail_setting('webhook_authorization', default=[], kwargs=kwargs)

                "You should set WEBHOOK_SECRET in your ANYMAIL settings. "
     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
        * *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
  *random:random* shared secret you created for your :setting:`ANYMAIL_WEBHOOK_SECRET`
     * *random:random* is an :setting:`ANYMAIL_WEBHOOK_SECRET` shared secret
    configure a **webhook secret** as described below.
   :setting:`WEBHOOK_SECRET <ANYMAIL_WEBHOOK_SECRET>`
          'WEBHOOK_SECRET': '<a random string>:<another random string>',
   with different ESPs. See :setting:`ANYMAIL_WEBHOOK_SECRET` in the
     * *random:random* is the WEBHOOK_SECRET string you created in step 1
.. rubric:: WEBHOOK_SECRET
will warn if you try to use webhooks without a shared secret.
.. versionchanged:: 1.4

    The earlier WEBHOOK_AUTHORIZATION setting was renamed WEBHOOK_SECRET, so that
    Django error reporting sanitizes it. The old name is still allowed in v1.4,
    but will be removed in a near-future release. You should update your settings.

.. setting:: ANYMAIL_WEBHOOK_SECRET
:setting:`!ANYMAIL_WEBHOOK_SECRET` setting.
          'WEBHOOK_SECRET': [
from django.core import checks
from django.test import SimpleTestCase
from django.test.utils import override_settings

from anymail.checks import check_deprecated_settings

from .utils import AnymailTestMixin


class DeprecatedSettingsTests(SimpleTestCase, AnymailTestMixin):
    @override_settings(ANYMAIL={"WEBHOOK_AUTHORIZATION": "abcde:12345"})
    def test_webhook_authorization(self):
        errors = check_deprecated_settings(None)
        self.assertEqual(errors, [checks.Warning(
            "The ANYMAIL setting 'WEBHOOK_AUTHORIZATION' has been renamed 'WEBHOOK_SECRET' to improve security.",
            hint="You must update your settings.py. The old name will stop working in a near-future release.",
            id="anymail.W001",
        )])

    @override_settings(ANYMAIL_WEBHOOK_AUTHORIZATION="abcde:12345", ANYMAIL={})
    def test_anymail_webhook_authorization(self):
        errors = check_deprecated_settings(None)
        self.assertEqual(errors, [checks.Warning(
            "The ANYMAIL_WEBHOOK_AUTHORIZATION setting has been renamed ANYMAIL_WEBHOOK_SECRET to improve security.",
            hint="You must update your settings.py. The old name will stop working in a near-future release.",
            id="anymail.W001",
        )])
    @override_settings(ANYMAIL={})  # clear WEBHOOK_SECRET from WebhookTestCase
            "WEBHOOK_SECRET": "abcde:12345",
    @override_settings(ANYMAIL={'WEBHOOK_SECRET': ['cred1:pass1', 'cred2:pass2']})
@override_settings(ANYMAIL={'WEBHOOK_SECRET': 'username:password'})
    @override_settings(ANYMAIL={'WEBHOOK_SECRET': ['cred1:pass1', 'cred2:pass2']})

    @override_settings(ANYMAIL={'WEBHOOK_AUTHORIZATION': "username:password"})
    def test_deprecated_setting(self):
        """The older WEBHOOK_AUTHORIZATION setting is still supported (for now)"""
        response = self.call_webhook()
        self.assertEqual(response.status_code, 200)