Mandrill: include auth in webhook signature calc

Mandrill's webhook signature calculation uses the
*exact url* Mandrill is posting to. If HTTP basic
auth is also used, that auth is included in the url.

Anymail was using Django's request.build_absolute_uri,
which doesn't include HTTP basic auth. Anymail now
includes the auth in the calculation, if it was present
in the request.

This should eliminate the need to use the
ANYMAIL_MANDRILL_WEBHOOK_URL override,
if Django's SECURE_PROXY_SSL_HEADER and
USE_X_FORWARDED_HOST (and/or
USE_X_FORWARDED_PROTO) settings are correct
for your server.

(The calculated url is now also included in
the validation failure error message, to aid
debugging.)

Fixes #48
This commit is contained in:
medmunds
2017-01-19 19:01:36 -08:00
parent 12660d3d4f
commit 0ba5d1d4ad
6 changed files with 163 additions and 24 deletions

View File

@@ -1,15 +1,14 @@
import base64
import re
import six
import warnings
import six
from django.http import HttpResponse
from django.utils.decorators import method_decorator
from django.views.decorators.csrf import csrf_exempt
from django.views.generic import View
from ..exceptions import AnymailInsecureWebhookWarning, AnymailWebhookValidationFailure
from ..utils import get_anymail_setting, collect_all_methods
from ..utils import get_anymail_setting, collect_all_methods, get_request_basic_auth
class AnymailBasicAuthMixin(object):
@@ -42,16 +41,8 @@ class AnymailBasicAuthMixin(object):
def validate_request(self, request):
"""If configured for webhook basic auth, validate request has correct auth."""
if self.basic_auth:
valid = False
try:
authtype, authdata = request.META['HTTP_AUTHORIZATION'].split()
if authtype.lower() == "basic":
auth = base64.b64decode(authdata).decode('utf-8')
if auth in self.basic_auth:
valid = True
except (IndexError, KeyError, TypeError, ValueError):
valid = False
if not valid:
basic_auth = get_request_basic_auth(request)
if basic_auth is None or basic_auth not in self.basic_auth:
# noinspection PyUnresolvedReferences
raise AnymailWebhookValidationFailure(
"Missing or invalid basic auth in Anymail %s webhook" % self.esp_name)

View File

@@ -10,7 +10,7 @@ from django.utils.timezone import utc
from .base import AnymailBaseWebhookView
from ..exceptions import AnymailWebhookValidationFailure, AnymailConfigurationError
from ..signals import tracking, AnymailTrackingEvent, EventType
from ..utils import get_anymail_setting, getfirst
from ..utils import get_anymail_setting, getfirst, get_request_uri
class MandrillSignatureMixin(object):
@@ -45,16 +45,18 @@ class MandrillSignatureMixin(object):
except KeyError:
raise AnymailWebhookValidationFailure("X-Mandrill-Signature header missing from webhook POST")
# Mandrill signs the exact URL plus the sorted POST params:
signed_data = self.webhook_url or request.build_absolute_uri()
# Mandrill signs the exact URL (including basic auth, if used) plus the sorted POST params:
url = self.webhook_url or get_request_uri(request)
params = request.POST.dict()
signed_data = url
for key in sorted(params.keys()):
signed_data += key + params[key]
expected_signature = b64encode(hmac.new(key=self.webhook_key, msg=signed_data.encode('utf-8'),
digestmod=hashlib.sha1).digest())
if not constant_time_compare(signature, expected_signature):
raise AnymailWebhookValidationFailure("Mandrill webhook called with incorrect signature")
raise AnymailWebhookValidationFailure(
"Mandrill webhook called with incorrect signature (for url %r)" % url)
class MandrillBaseWebhookView(MandrillSignatureMixin, AnymailBaseWebhookView):