Opened 3 years ago
Last modified 7 days ago
#34753 assigned Cleanup/optimization
Document how to safely construct email addresses
| Reported by: | Sylvain Fankhauser | Owned by: | Mike Edmunds |
|---|---|---|---|
| Component: | Documentation | Version: | |
| Severity: | Normal | Keywords: | |
| Cc: | Mike Edmunds | Triage Stage: | Accepted |
| 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 (13)
comment:1 by , 3 years ago
| Resolution: | → invalid |
|---|---|
| Status: | new → closed |
comment:2 by , 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 , 7 months ago
| Cc: | added |
|---|---|
| Resolution: | invalid |
| Status: | closed → new |
| Summary: | Document how to properly escape `to` in email messages → Document how to safely construct email addresses |
| Triage Stage: | Unreviewed → Accepted |
| Type: | Uncategorized → Cleanup/optimization |
| Version: | 4.2 |
Reopening and accepting per forum discussion, see Mike's implementation advice there.
comment:4 by , 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.)
comment:5 by , 6 months ago
| Owner: | changed from to |
|---|---|
| Status: | new → assigned |
I’m planning to work on updating the email documentation to cover safe construction of email addresses, incorporating the suggestions above.
comment:6 by , 6 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 , 6 months ago
| Has patch: | set |
|---|
comment:9 by , 4 months ago
| Keywords: | email added |
|---|---|
| Patch needs improvement: | set |
comment:10 by , 2 weeks ago
| Owner: | removed |
|---|---|
| Status: | assigned → new |
comment:11 by , 2 weeks ago
| Owner: | set to |
|---|---|
| Status: | new → assigned |
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
ValueErrorif 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.Addressobject or (if you don't need IDN support) the legacyemail.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:
- https://beaglesecurity.com/blog/vulnerability/email-header-injection.html
- https://www.invicti.com/learn/email-injection
- https://www.acunetix.com/blog/articles/email-header-injection/ (same content, related company, different formatting)
- https://en.wikipedia.org/wiki/Email_injection (stub article)
comment:12 by , 7 days ago
| Patch needs improvement: | unset |
|---|
comment:13 by , 7 days 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.)
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.