CVE-2018-1000089
Description
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.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Anymail django-anymail versions 0.2 through 1.3 expose the WEBHOOK_AUTHORIZATION secret in error logs, allowing attackers to forge tracking events.
Vulnerability
Anymail django-anymail versions 0.2 through 1.3 contain a CWE-532 (Insertion of Sensitive Information into Log File) and CWE-209 (Information Exposure Through an Error Message) vulnerability in the handling of the WEBHOOK_AUTHORIZATION setting value [1], [2]. The setting is intended to authenticate webhook requests from email service providers, but the value is inadvertently exposed in Django error reports when exceptions occur. This allows the secret to be written to logs or displayed in debug error pages if Django's DEBUG mode is enabled or error logging includes the setting value [1]. The fix was introduced in v1.4 [1].
Exploitation
An attacker with access to the application's error logs or debug error pages can discover the WEBHOOK_AUTHORIZATION setting value [1]. This access does not require authentication to the webhook endpoint itself; the attacker only needs to be able to read logged error output or observe debug responses. Once the secret is obtained, the attacker can craft HTTP requests that include the secret as HTTP Basic Auth credentials, posing as a legitimate email service provider. They can then send fabricated or malicious Anymail tracking and inbound events to the application's webhook endpoint [1].
Impact
Successful exploitation allows the attacker to inject arbitrary email tracking events (e.g., delivery, bounce, open, click) or inbound email data into the Django application [1]. This can lead to incorrect analytics, trigger unintended business logic (e.g., marking an order as delivered), or process malicious inbound email content. The integrity of the application's email tracking and inbound email handling is compromised. No remote code execution or privilege escalation is described in the references, but the attacker can forge event data within the context of the Anymail integration.
Mitigation
The vulnerability is fixed in django-anymail version 1.4, which renames the setting from WEBHOOK_AUTHORIZATION to WEBHOOK_SECRET and ensures the secret is not exposed in error output [1], [4]. Users of versions 0.2 through 1.3 should upgrade to v1.4 or later immediately [1]. As a workaround, users should ensure Django's DEBUG mode is disabled in production and that error logs do not contain sensitive settings, but upgrading is the definitive fix. There is no indication that this CVE is listed in the KEV catalog.
AI Insight generated on May 22, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
django-anymailPyPI | >= 0.2, < 1.4 | 1.4 |
Affected products
2- Range: >=0.2, <=1.3
Patches
11a6086f2b584Security: rename WEBHOOK_AUTHORIZATION --> WEBHOOK_SECRET
14 files changed · +99 −29
anymail/apps.py+4 −1 modified@@ -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)
anymail/checks.py+24 −0 added@@ -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
anymail/webhooks/base.py+6 −2 modified@@ -24,15 +24,19 @@ class AnymailBasicAuthMixin(object): 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
docs/esps/mailgun.rst+2 −2 modified@@ -197,7 +197,7 @@ 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 @@ -232,7 +232,7 @@ 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/)
docs/esps/mailjet.rst+2 −2 modified@@ -232,7 +232,7 @@ the url in your Mailjet account REST API settings under `Event tracking (trigger :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. @@ -263,7 +263,7 @@ 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
docs/esps/mandrill.rst+1 −1 modified@@ -206,7 +206,7 @@ requires deploying your Django project twice: :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.)
docs/esps/postmark.rst+2 −2 modified@@ -181,7 +181,7 @@ 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" @@ -216,7 +216,7 @@ 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
docs/esps/sendgrid.rst+2 −2 modified@@ -284,7 +284,7 @@ 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. @@ -315,7 +315,7 @@ 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
docs/esps/sparkpost.rst+2 −2 modified@@ -197,7 +197,7 @@ 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 @@ -235,7 +235,7 @@ 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:
docs/installation.rst+13 −7 modified@@ -98,22 +98,22 @@ Skip this section if you won't be using Anymail's webhooks. 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, @@ -133,7 +133,7 @@ If you want to use Anymail's inbound or tracking webhooks: (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.) @@ -160,7 +160,7 @@ If you want to use Anymail's inbound or tracking webhooks: :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") @@ -266,20 +266,26 @@ 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
docs/tips/securing_webhooks.rst+3 −3 modified@@ -29,7 +29,7 @@ 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 --------------------------------- @@ -41,7 +41,7 @@ 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. @@ -60,7 +60,7 @@ any of the authorization strings: ANYMAIL = { ... - 'WEBHOOK_AUTHORIZATION': [ + 'WEBHOOK_SECRET': [ 'abcdefghijklmnop:qrstuvwxyz0123456789', 'ZYXWVUTSRQPONMLK:JIHGFEDCBA9876543210', ],
tests/test_checks.py+27 −0 added@@ -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", + )])
tests/test_mandrill_webhooks.py+3 −3 modified@@ -87,7 +87,7 @@ def test_verifies_bad_signature(self): 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() @@ -99,7 +99,7 @@ def test_no_basic_auth(self): 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 @@ -111,7 +111,7 @@ def test_webhook_url_setting(self): 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')
tests/webhook_cases.py+8 −2 modified@@ -14,7 +14,7 @@ def event_handler(sender, event, esp_name, **kwargs): pass -@override_settings(ANYMAIL={'WEBHOOK_AUTHORIZATION': 'username:password'}) +@override_settings(ANYMAIL={'WEBHOOK_SECRET': 'username:password'}) class WebhookTestCase(AnymailTestMixin, SimpleTestCase): """Base for testing webhooks @@ -111,7 +111,7 @@ def test_verifies_missing_auth(self): 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') @@ -125,3 +125,9 @@ def test_supports_credential_rotation(self): 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)
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
5- github.com/advisories/GHSA-qh9x-mc42-vg4gghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2018-1000089ghsaADVISORY
- github.com/anymail/django-anymail/commit/1a6086f2b58478d71f89bf27eb034ed81aefe5efghsax_refsource_MISCWEB
- github.com/anymail/django-anymail/releases/tag/v1.4ghsax_refsource_MISCWEB
- github.com/pypa/advisory-database/tree/main/vulns/django-anymail/PYSEC-2018-46.yamlghsaWEB
News mentions
0No linked articles in our index yet.