Incorrect signature verification in django-ses
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].
- GitHub - django-ses/django-ses: A Django email backend for Amazon's Simple Email Service
- django-ses/CVE/001-cert-url-signature-verification.md at 3d627067935876487f9938310d5e1fbb249a7778 · django-ses/django-ses
- NVD - CVE-2023-33185
- Restrict amazonaws allowed certificate URLs. · django-ses/django-ses@b71b5f4
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.
| Package | Affected versions | Patched versions |
|---|---|---|
django-sesPyPI | < 3.5.0 | 3.5.0 |
Affected products
3- Range: <3.5.0
- django-ses/django-sesv5Range: < 3.5.0
Patches
1b71b5f413293Restrict amazonaws allowed certificate URLs.
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- github.com/advisories/GHSA-qg36-9jxh-fj25ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2023-33185ghsaADVISORY
- github.com/django-ses/django-ses/blob/3d627067935876487f9938310d5e1fbb249a7778/CVE/001-cert-url-signature-verification.mdghsax_refsource_MISCWEB
- github.com/django-ses/django-ses/commit/b71b5f413293a13997b6e6314086cb9c22629795ghsax_refsource_MISCWEB
- github.com/django-ses/django-ses/security/advisories/GHSA-qg36-9jxh-fj25ghsax_refsource_CONFIRMWEB
- github.com/pypa/advisory-database/tree/main/vulns/django-ses/PYSEC-2023-82.yamlghsaWEB
News mentions
0No linked articles in our index yet.