CVE-2018-1000089 (Other, published 2018-03-13T15:29Z)
Description: 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.
Severity: MEDIUM (CVSS v3 base score: 7.4)
Weakness: CWE-532 \u2014 Insertion of Sensitive Information into Log File
Information written to log files can be of a sensitive nature and give valuable guidance to an attacker or expose sensitive user information.
Repository: https://github.com/anymail/django-anymail
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 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


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)