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: