Opened 3 years ago

Closed 8 days ago

Last modified 8 days ago

#34753 closed Cleanup/optimization (fixed)

Document how to safely construct email addresses

Reported by: Sylvain Fankhauser Owned by: Mike Edmunds
Component: Documentation Version: dev
Severity: Normal Keywords: email
Cc: Mike Edmunds Triage Stage: Ready for checkin
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

The documentation about sending email (https://docs.djangoproject.com/en/dev/topics/email/) only provides examples with recipients being only e-mail addresses, without the recipient name. I believe adding the name of the recipient to the To header is a standard practice, and I think Django could provide some guidance on how to escape it properly since it can easily be misused.

For example, a naive way of doing it would be to use f"{first_name} {last_name} <{email}>" (which will fail if first_name, last_name or email contain special characters such as <, >, " or ,. I’m actually guilty of using this in the past, only to find out at my own expense that this wasn’t a good idea). Another way would be to pass the result of sanitize_address((f"{first_name} {last_name}", email), "utf-8") to the to argument, which would work until someone has a name that’s long enough for sanitize_address to add a \n character in the middle, resulting in an error when sanitize_address will be called a second time when actually sending the mail.

I’m still not entirely sure of the proper way to do it properly (and I’m actually surprised I couldn’t find anything about this online). I think the proper way to do it would be to pass the result of email.utils.formataddr((f"{first_name} {last_name}", email)) to the to argument. If you think that’s the correct way to do it and you think the docs could be improved by adding a note about this, I can take care of submitting a patch.

Change History (20)

comment:1 by Mariusz Felisiak, 3 years ago

Resolution: invalid
Status: newclosed

Thanks for the ticket, however it's rather a support question. Django is not a mail server and we cannot document all related caveats, best practices, and how-to's.

Closing per TicketClosingReasons/UseSupportChannels.

comment:2 by Claude Paroz, 3 years ago

I would not be so categorical, I think that this is a common use case and a note in the docs wouldn't hurt. Maybe the note would simply redirect to an external reference (Python docs or RFC).

comment:3 by Jacob Walls, 7 months ago

Cc: Mike Edmunds added
Resolution: invalid
Status: closednew
Summary: Document how to properly escape `to` in email messagesDocument how to safely construct email addresses
Triage Stage: UnreviewedAccepted
Type: UncategorizedCleanup/optimization
Version: 4.2

Reopening and accepting per forum discussion, see Mike's implementation advice there.

comment:4 by Mike Edmunds, 7 months ago

I would suggest reworking the entire existing "Preventing header injection" section as part of this change. Both the text and example can be improved.

A more useful example might be actually treating it as a typical contact form, with name, email, subject and message fields:

  • from_email would be f'"{name} via contact form" <contact-form@example.com> (but formatted safely)
  • to would be a constant (["contact@example.com"] or something like that)
  • reply_to would be [f"{name} <{email}>"] (but formatted safely)
  • subject & body would come from the form

This corrects another problem in the current example: trying to use an email from a web form as the from_email. (No email service lets you send messages from any random address. But people building contact forms often think that's how to do it.)

Last edited 7 months ago by Mike Edmunds (previous) (diff)

comment:5 by Raghav Bodani, 7 months ago

Owner: changed from nobody to Raghav Bodani
Status: newassigned

I’m planning to work on updating the email documentation to cover safe construction of email addresses, incorporating the suggestions above.

comment:6 by Raghav Bodani, 7 months ago

Quick update: the first draft of the documentation changes is ready. I will do a final review and open a PR once it’s ready.

comment:8 by Raghav Bodani, 7 months ago

Has patch: set

comment:9 by Mike Edmunds, 5 months ago

Keywords: email added
Patch needs improvement: set

comment:10 by Jacob Walls, 5 weeks ago

Owner: Raghav Bodani removed
Status: assignednew

comment:11 by Mike Edmunds, 5 weeks ago

Owner: set to Mike Edmunds
Status: newassigned

Copying in some of my notes (from a comment in the earlier PR):

I think we need to substantially rework the whole text in the section, so it emphasizes the part that's the *developer's* responsibility (preventing email address syntax injection). I'm not sure how best to word it, but here's what I'd want to convey and roughly the order:

  • Email header injection is a security exploit in which an attacker manipulates email headers to change the intended sender, recipients, other headers, or even the entire message.
  • Header injection can be caused by newlines in header values (CRLF injection) or by certain characters like commas and parentheses in address headers (address syntax injection). [There doesn't seem to be a standard term for the second type. We could instead say "header content injection" or "delimiter injection".]
  • Django builds on Python's email library, which prevents CRLF injection. You'll get a ValueError if you try to send a message with newlines in header values.
  • But Django can't protect you from address syntax injection. You are responsible for properly sanitizing email addresses constructed from user-supplied or variable data.
  • The best way to construct email addresses is using a well-tested library function intended for the purpose, like Python's email.headerregistry.Address object or (if you don't need IDN support) the legacy email.utils.formataddr() function. (See example below.)
  • Never try to use string formatting like f'"{name}" <{email}>' to compose an email address header. This is unsafe, just like building SQL or HTML with string formatting.
  • If you're sending email that includes user-supplied content, you'll likely need to take steps to prevent spoofing, spear-phishing, spam cannons, and other malicious uses of email. (Details are beyond the scope of Django docs.)
  • Here's an example contact form that demonstrates some of these concepts…

Also, I've been trying to find a replacement link for http://www.nyphp.org/phundamentals/8_Preventing-Email-Header-Injection.html, which is heavily PHP centric and a bit outdated. All the general descriptions I could find cover only CRLF injection, not address syntax injection. (And they all use similar PHP code examples.) But maybe one of these? Most are commercial sites; don't know if we have a policy about that:

comment:12 by Mike Edmunds, 4 weeks ago

Patch needs improvement: unset

comment:13 by Mike Edmunds, 4 weeks ago

I ended up removing the contact-form-like example entirely. It ended up not helpful. (And we have a better contact form example in the forms docs, though that has some of the issues in my earlier comment. See #37162.)

comment:14 by Natalia Bidart, 10 days ago

Triage Stage: AcceptedReady for checkin
Version: dev

comment:15 by Mike Edmunds, 10 days ago

Patch needs improvement: set
Triage Stage: Ready for checkinAccepted

comment:16 by Mike Edmunds, 9 days ago

Patch needs improvement: unset

comment:17 by Natalia Bidart, 8 days ago

Triage Stage: AcceptedReady for checkin

comment:18 by nessita <124304+nessita@…>, 8 days ago

Resolution: fixed
Status: assignedclosed

In f3f9601c:

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.

comment:19 by Natalia <124304+nessita@…>, 8 days ago

In 50ea553:

[6.1.x] 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.

Backport of f3f9601cff03b38942e70ce8042bdfdec6002449 from main.

comment:20 by Natalia Bidart, 8 days ago

I won't backport these changes to stable/6.0.x because docs/topics/email.txt is considerably different following the MAILERS work.

Note: See TracTickets for help on using tickets.
Back to Top