VYPR
Low severityNVD Advisory· Published May 26, 2023· Updated Jan 14, 2025

Incorrect signature verification in django-ses

CVE-2023-33185

Description

Django-SES is a drop-in mail backend for Django. The django_ses library implements a mail backend for Django using AWS Simple Email Service. The library exports the SESEventWebhookView class intended to receive signed requests from AWS to handle email bounces, subscriptions, etc. These requests are signed by AWS and are verified by django_ses, however the verification of this signature was found to be flawed as it allowed users to specify arbitrary public certificates. This issue was patched in version 3.5.0.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

Django-SES webhook signature verification flaw allowed arbitrary public certificate URLs, enabling forged AWS SNS event processing.

Vulnerability

Overview CVE-2023-33185 affects Django-SES, a Django mail backend for AWS Simple Email Service (SES). The SESEventWebhookView class receives signed AWS SNS notifications (bounces, complaints, etc.) and verifies their signatures using the public certificate URL contained in the request. The signature verification function _get_cert_url validated the certificate URL only against a list of trusted domains (by default amazonaws.com) via a suffix match, without enforcing a strict path or subdomain pattern. This allowed an attacker to host a malicious PEM certificate on any S3 bucket (e.g., attacker-bucket.s3.amazonaws.com) and craft a request that would pass the domain check, as s3.amazonaws.com ends with amazonaws.com [1][2].

Exploitation

Details An unauthenticated attacker who can send a crafted HTTP POST request to the webhook endpoint can exploit this vulnerability. The attacker includes a SigningCertURL pointing to their own certificate hosted on an arbitrary AWS S3 bucket (or any service with a domain ending in amazonaws.com). Since the verification process downloads and uses the certificate from that URL without further validation (e.g., matching known AWS SNS certificate patterns), the attacker can sign arbitrary notification payloads that will be accepted as legitimate by Django-SES [2]. No authentication is required, as the webhook endpoint is typically exposed to the internet to receive AWS SNS messages.

Impact

Successful exploitation allows an attacker to inject fake SES event notifications (e.g., bounces, complaints, delivery notifications) into the Django application. This can lead to incorrect email deliverability tracking, bypass of reputation monitoring, or other workflow disruptions that depend on these webhooks. In some configurations, it may also enable more serious side effects if the application logic processes these events in critical ways (e.g., automatically disabling email addresses) [1][3].

Mitigation

The vulnerability is patched in Django-SES version 3.5.0. The fix introduces a strict regular expression (SES_REGEX_CERT_URL) that only allows certificate URLs from the official AWS SNS endpoint pattern: https://sns.[region].amazonaws.com/SimpleNotificationService-[id].pem [4]. Users should upgrade to v3.5.0 or later. As a workaround, administrators can manually configure AWS_SNS_EVENT_CERT_TRUSTED_DOMAINS to include only specific, non-wildcarded domains, though the patch is strongly recommended [2].

AI Insight generated on May 20, 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.

PackageAffected versionsPatched versions
django-sesPyPI
< 3.5.03.5.0

Affected products

3

Patches

1
b71b5f413293

Restrict amazonaws allowed certificate URLs.

https://github.com/django-ses/django-sesPaul CraciunoiuMay 21, 2023via ghsa
1 file changed · +50 32
  • django_ses/utils.py+50 32 modified
    @@ -1,21 +1,25 @@
     import base64
     import logging
    +import re
     import warnings
     from builtins import bytes
    -
    -from django_ses.deprecation import RemovedInDjangoSES20Warning
    -
    +from urllib.error import URLError
     from urllib.parse import urlparse
     from urllib.request import urlopen
    -from urllib.error import URLError
     
     from django.core.exceptions import ImproperlyConfigured
    +
     from django_ses import settings
    +from django_ses.deprecation import RemovedInDjangoSES20Warning
     
     logger = logging.getLogger(__name__)
     
     _CERT_CACHE = {}
     
    +SES_REGEX_CERT_URL = re.compile(
    +    "(?i)^https://sns\.[a-z0-9\-]+\.amazonaws\.com(\.cn)?/SimpleNotificationService\-[a-z0-9]+\.pem$"
    +)
    +
     
     def clear_cert_cache():
         """Clear the certificate cache.
    @@ -183,6 +187,17 @@ def _get_cert_url(self):
             url_obj = urlparse(cert_url)
             for trusted_domain in settings.EVENT_CERT_DOMAINS:
                 parts = trusted_domain.split(".")
    +            if "amazonaws.com" in trusted_domain:
    +                if not SES_REGEX_CERT_URL.match(cert_url):
    +                    if len(parts) < 4:
    +                        return None
    +                    else:
    +                        logger.warning('Possible security risk for: "%s"', cert_url)
    +                        logger.warning(
    +                            "It is strongly recommended to configure the full domain in EVENT_CERT_DOMAINS. "
    +                            "See v3.5.0 release notes for more details."
    +                        )
    +
                 if url_obj.netloc.split(".")[-len(parts) :] == parts:
                     return cert_url
     
    @@ -196,26 +211,28 @@ def _get_bytes_to_sign(self):
     
             # Depending on the message type the fields to add to the message
             # differ so we handle that here.
    -        msg_type = self._data.get('Type')
    -        if msg_type == 'Notification':
    +        msg_type = self._data.get("Type")
    +        if msg_type == "Notification":
                 fields_to_sign = [
    -                'Message',
    -                'MessageId',
    -                'Subject',
    -                'Timestamp',
    -                'TopicArn',
    -                'Type',
    +                "Message",
    +                "MessageId",
    +                "Subject",
    +                "Timestamp",
    +                "TopicArn",
    +                "Type",
                 ]
    -        elif (msg_type == 'SubscriptionConfirmation' or
    -              msg_type == 'UnsubscribeConfirmation'):
    +        elif (
    +            msg_type == "SubscriptionConfirmation"
    +            or msg_type == "UnsubscribeConfirmation"
    +        ):
                 fields_to_sign = [
    -                'Message',
    -                'MessageId',
    -                'SubscribeURL',
    -                'Timestamp',
    -                'Token',
    -                'TopicArn',
    -                'Type',
    +                "Message",
    +                "MessageId",
    +                "SubscribeURL",
    +                "Timestamp",
    +                "Token",
    +                "TopicArn",
    +                "Type",
                 ]
             else:
                 # Unrecognized type
    @@ -237,14 +254,14 @@ def _get_bytes_to_sign(self):
     
     def BounceMessageVerifier(*args, **kwargs):
         warnings.warn(
    -        'utils.BounceMessageVerifier is deprecated. It is renamed to EventMessageVerifier.',
    +        "utils.BounceMessageVerifier is deprecated. It is renamed to EventMessageVerifier.",
             RemovedInDjangoSES20Warning,
         )
     
         # parameter name is renamed from bounce_dict to notification.
    -    if 'bounce_dict' in kwargs:
    -        kwargs['notification'] = kwargs['bounce_dict']
    -        del kwargs['bounce_dict']
    +    if "bounce_dict" in kwargs:
    +        kwargs["notification"] = kwargs["bounce_dict"]
    +        del kwargs["bounce_dict"]
     
         return EventMessageVerifier(*args, **kwargs)
     
    @@ -262,31 +279,32 @@ def verify_bounce_message(msg):
         Verify an SES/SNS bounce(event) notification message.
         """
         warnings.warn(
    -        'utils.verify_bounce_message is deprecated. It is renamed to verify_event_message.',
    +        "utils.verify_bounce_message is deprecated. It is renamed to verify_event_message.",
             RemovedInDjangoSES20Warning,
         )
         return verify_event_message(msg)
     
     
     def confirm_sns_subscription(notification):
         logger.info(
    -        'Received subscription confirmation: TopicArn: %s',
    -        notification.get('TopicArn'),
    +        "Received subscription confirmation: TopicArn: %s",
    +        notification.get("TopicArn"),
             extra={
    -            'notification': notification,
    +            "notification": notification,
             },
         )
     
         # Get the subscribe url and hit the url to confirm the subscription.
    -    subscribe_url = notification.get('SubscribeURL')
    +    subscribe_url = notification.get("SubscribeURL")
         try:
             urlopen(subscribe_url).read()
         except URLError as e:
             # Some kind of error occurred when confirming the request.
             logger.error(
    -            'Could not confirm subscription: "%s"', e,
    +            'Could not confirm subscription: "%s"',
    +            e,
                 extra={
    -                'notification': notification,
    +                "notification": notification,
                 },
                 exc_info=True,
             )
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

6

News mentions

0

No linked articles in our index yet.