From f3f9601cff03b38942e70ce8042bdfdec6002449 Mon Sep 17 00:00:00 2001 From: Mike Edmunds Date: Fri, 12 Jun 2026 11:20:45 -0700 Subject: [PATCH] Fixed #34753 -- Extended security and safety remarks in email topics docs. Reworked the outdated "Preventing header injection" section: * Added a "Safely sending email" section noting that the topic is relevant but beyond the scope of Django's own docs. * Added a section on correctly formatting email addresses with a variable display name to avoid injection attacks. * Updated the existing "Preventing header injection" section to note that Django (via Python) already prevents CRLF injection, but that custom email backends that bypass Django's protections may need to handle it. Also added references to the new "Formatting email addresses" section from the `ADMINS`, `DEFAULT_FROM_EMAIL`, and `SERVER_EMAIL` settings. --- docs/ref/settings.txt | 11 ++++ docs/topics/email.txt | 138 +++++++++++++++++++++++++++++++----------- 2 files changed, 113 insertions(+), 36 deletions(-) diff --git a/docs/ref/settings.txt b/docs/ref/settings.txt index 31762338b99a..8857d063c5c3 100644 --- a/docs/ref/settings.txt +++ b/docs/ref/settings.txt @@ -56,6 +56,11 @@ Each item in the list should be an email address string. Example:: ADMINS = ["john@example.com", '"Ng, Mary" '] +To safely build an address with a display name from variable content, see +:ref:`formatting-addresses`, which recommends using +:class:`~email.headerregistry.Address` over string formatting, but since this +setting requires a list of strings, wrap each ``Address`` in ``str(...)``. + .. setting:: ALLOWED_HOSTS ``ALLOWED_HOSTS`` @@ -1351,6 +1356,9 @@ any format valid in the chosen email sending protocol. This doesn't affect error messages sent to :setting:`ADMINS` and :setting:`MANAGERS`. See :setting:`SERVER_EMAIL` for that. +To build an address with a display name from variable content, see +:ref:`formatting-addresses`. + .. setting:: DEFAULT_INDEX_TABLESPACE ``DEFAULT_INDEX_TABLESPACE`` @@ -2797,6 +2805,9 @@ The email address that error messages come from, such as those sent to ``From:`` header and can take any format valid in the chosen email sending protocol. +To build an address with a display name from variable content, see +:ref:`formatting-addresses`. + .. admonition:: Why are my emails sent from a different address? This address is used only for error messages. It is *not* the address that diff --git a/docs/topics/email.txt b/docs/topics/email.txt index c9e3cfd1cef0..b8446d908877 100644 --- a/docs/topics/email.txt +++ b/docs/topics/email.txt @@ -495,7 +495,8 @@ email backend API :ref:`provides an alternative * ``body``: The body text. This should be a plain text message. * ``from_email``: The sender's address. Both ``fred@example.com`` and - ``"Fred" `` forms are legal. If omitted, the + ``"Fred" `` forms are supported (see + :ref:`formatting-addresses`). If omitted, the :setting:`DEFAULT_FROM_EMAIL` setting is used. * ``to``: A list or tuple of recipient addresses. @@ -792,47 +793,112 @@ example:: msg.content_subtype = "html" # Main content is now text/html msg.send() -Preventing header injection ---------------------------- +Safely sending email +-------------------- + +Any public website that can send email will eventually be targeted by attempts +to abuse it for spam, phishing, or other malicious content. While a complete +discussion of vulnerabilities in sending email is beyond the scope of Django's +documentation, there are many references available on the web. Two good +starting points are: + +* Princeton University's guidance on `preventing email abuse in web forms`_. + Although this is an internal reference for users of Princeton's Drupal Site + Builder, nearly all of its advice applies equally to sites built with Django + (or any web framework). + +* OWASP's `Email Validation and Verification in Identity Systems Cheat Sheet`_. + This primarily covers using email in authentication contexts. While many of + its recommendations are handled by :mod:`django.contrib.auth` and other + Django features, items like rate limiting and securing email change workflows + are the developer's responsibility. + +Thinking through how email might be abused (and taking steps to mitigate it) is +especially important if your site can send to unverified addresses. Features +like newsletter sign-up, contact forms that cc or auto-reply to the sender, and +"share this page" can be attractive targets. + +.. _preventing email abuse in web forms: + https://sitebuilder.princeton.edu/site-administration/spam +.. _Email Validation and Verification in Identity Systems Cheat Sheet: + https://cheatsheetseries.owasp.org/cheatsheets/Email_Validation_and_Verification_Cheat_Sheet.html + +.. _formatting-addresses: + +Formatting email addresses +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Email addresses allow a "friendly" display name alongside the ``user@domain`` +address. For example, you could include your company name in the +:setting:`DEFAULT_FROM_EMAIL` setting:: + + DEFAULT_FROM_EMAIL = '"Example, Inc." ' + +The double quotes around ``"Example, Inc."`` are needed so the comma isn't read +as separating two different addresses. A fixed address, written out by hand as +in the example above, is safe, but composing one from variable parts +(especially untrusted input) needs more care. -`Header injection`_ is a security exploit in which an attacker inserts extra -email headers to control the "To:" and "From:" in email messages that your -scripts generate. +.. warning:: -The Django email functions outlined above all protect against header injection -by forbidding newlines in header values. If any ``subject``, ``from_email`` or -``recipient_list`` contains a newline (in either Unix, Windows or Mac style), -the email function (e.g. :func:`send_mail`) will raise :exc:`ValueError` and, -hence, will not send the email. It's your responsibility to validate all data -before passing it to the email functions. + Never use string formatting to build an email address from variable parts. + For example, ``f'"{name}" <{email}>'`` is **unsafe**. -If a ``message`` contains headers at the start of the string, the headers will -be printed as the first bit of the email message. +Email address headers have complex syntax rules (much like HTML or SQL), so +constructing them by combining strings creates an injection vulnerability. Even +if you've :class:`validated <.EmailValidator>` the format of ``email``, an +attacker could exploit the ``name`` portion to inject additional addresses. -Here's an example view that takes a ``subject``, ``message`` and ``from_email`` -from the request's POST data, sends that to ``admin@example.com`` and redirects -to "/contact/thanks/" when it's done:: +To avoid this, always use a well-tested library specifically meant to format +email addresses, like Python's :class:`email.headerregistry.Address` class (the +replacement for the legacy :func:`~email.utils.formataddr` function, which does +not support internationalized domain names). + +For example, to include a user's full name when sending them email (where +``user`` is an instance of the default :class:`.User` model):: from django.core.mail import send_mail - from django.http import HttpResponse, HttpResponseRedirect - - - def send_email(request): - subject = request.POST.get("subject", "") - message = request.POST.get("message", "") - from_email = request.POST.get("from_email", "") - if subject and message and from_email: - try: - send_mail(subject, message, from_email, ["admin@example.com"]) - except ValueError: - return HttpResponse("Invalid header found.") - return HttpResponseRedirect("/contact/thanks/") - else: - # In reality we'd use a form class - # to get proper validation errors. - return HttpResponse("Make sure all fields are entered and valid.") - -.. _Header injection: http://www.nyphp.org/phundamentals/8_Preventing-Email-Header-Injection.html + from email.headerregistry import Address + + + def send_mail_to_user(user, subject, body, from_email=None): + # Safely create an email address with the user's name. + # (addr_spec is the technical term for the user@domain address.) + address = Address( + display_name=user.get_full_name(), + addr_spec=user.email, + ) + send_mail(subject, body, from_email, [address]) + +Django's built-in email backends support using +:class:`~email.headerregistry.Address` objects directly in any address field, +as shown here. So do many custom and third-party email backends. But if this +causes a ``TypeError`` or other problem with a particular backend, use +``str(address)`` to convert the object to a safe, properly formatted string. + +Preventing header injection +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +`Email header injection`_ is a security exploit in which an attacker +manipulates email headers to change the intended sender or recipients, subject, +or potentially even the entire visible message body. + +One type of header injection exploits address header syntax. You are +responsible for preventing this when constructing email addresses from +user-supplied input, as described in :ref:`formatting-addresses` above. + +Another (perhaps better understood) attack, CRLF injection, uses carriage +return and line feed characters to insert additional headers into the email. +Django prevents this by raising a :exc:`ValueError` if those characters appear +in any header field when trying to send the message. + +Django's CRLF protection relies on using Python's modern +:class:`~email.policy.EmailPolicy` in Django's :meth:`EmailMessage.message`. +Custom email backends that don't call that function, or that call it with the +legacy :data:`~email.policy.compat32` policy, are responsible for implementing +their own CRLF injection prevention. + +.. _Email header injection: https://en.wikipedia.org/wiki/Email_injection .. _topics-sending-multiple-emails: