Don't let your users get pwned via email HTML injection
Niels Swimberghe - - Web
Follow me on Twitter, buy me a coffee
This blog post was written for Twilio and originally published at the Twilio blog.
Every few years, the Open Web Application Security Project (OWASP) publishes a new list of the 10 most common security issues in web applications, called OWASP Top 10. There is one security flaw that has been around since the first edition in 2003, and grabbed the first spot in the 2010, 2013, and 2017 edition, and that security issue is vulnerability to injection attacks.
What are injection attacks #
An injection attack occurs when an application accepts input and malicious users submit code into the input in an attempt to have the application execute the code. An application is vulnerable to this type of attack when the input provided by the user is embedded into application code that is then interpreted.
The most well-known type of injection attack is SQL injection, which can occur when user input is embedded into the SQL queries. This allows malicious users to modify the SQL query before it is sent off to the SQL server. The best way to prevent SQL injection is by, instead of embedding user input into the SQL queries, you pass in the user input through parameters, this is called Query Parameterization.
Another well-known type of injection attack is Cross Site Scripting (XSS), which can occur when a website accepts user input and that user input is then embedded into HTML or JavaScript. A malicious user can submit JavaScript to the user input, which will then be embedded into web pages, which executes the JavaScript in the browser of unknowing victims.
To prevent XSS, you have to encode the user input before returning it to the user. If you embed user input in:
- HTML - HTML-encode the input.
- HTML attributes - attribute-encode the input.
- JavaScript - JavaScript-encode the input.
Historically, XSS has been ranked as its own security vulnerability in OWASP Top 10, but in the 2021 edition of OWASP Top 10, XSS has been merged into the injection vulnerability.
There are many types of injection attacks, but one thing is consistent across all of them, they all accept user input and then embed user input into code. So, never trust user input, avoid embedding user input into any type of code, but if you have to, research how that could be abused and apply preventative measures.
This is lesser known, but similar to XSS attacks where HTML can be injected through user input into websites, HTML can also be injected into emails.
How HTML injection into emails work #
HTML injection is a vulnerability where an application accepts user input and then embeds the input into HTML. A malicious user can inject HTML through the user input so that their malicious HTML is embedded into the overall HTML generated by the application. Most commonly, this type of attack is used to embed malicious HTML including JavaScript into a website which makes it a XSS attack, but it could also be used to inject malicious HTML into emails.
Emails can be sent using two different content-types, plain text and HTML. If the email is in plain-text, injected HTML will be rendered as text and not rendered as HTML. HTML emails, on the other hand, are at risk, because the injected HTML will be rendered as part of the overall HTML email. What's worse is that some email clients will render style-blocks, which allows the attacker to take further control over how the email looks.
Consider this use-case: A user can fill out a form to sign up for a newsletter. The form collects the user's first name, last name, and email address. This user input is used to send a confirmation email using the following code:
public void OnFormSubmit(string firstName, string lastName, string emailAddress) { var subject = $"{firstName}, confirm your newsletter subscription"; var htmlBody = $"Hi {firstName} {lastName}, <br />" + "Thank you for signing up for our newsletter. Please click the link below to confirm your subscription. <br />" + "<a href=\"https://some_url/confirm?token=???\">Confirm your subscription</a>"; SendEmail(subject, htmlBody, emailAddress); }
If a user signs up for the newsletter, this is the resulting email:
However, if a malicious user enters HTML into the form, the email could look like this:
I achieved this result by entering the following values into the form:
- First name: Your bill is due
- Last name:
<style>*{color: transparent;} #bill{color: #000;} #bill a{color: blue;}</style> <p id="bill">Your bill is due, <a href="https://localhost/malicious_site">Pay now</a> or we will charge a late fee.</p>
- Email Address: [victim's email address]
At the end of the first name field, I added whitespace in an attempt to hide the rest of the subject, and in the last name field, I entered CSS to hide everything, except for my malicious payment notice.
However, when you open the same email on the Gmail client, you'll notice that Gmail filtered out the style block while still rendering the rest of the malicious HTML, which results in an email that is still malicious, but looks more suspicious.
This use-case is especially dangerous because the attacker can freely specify the victim's email address in the form. Avoid letting your users specify to whom the email should be sent if possible. In case of a newsletter signup, there's no way around prompting the user for their email address, so these use-cases should be reviewed and monitored with extra caution.
Why HTML injection into emails is dangerous #
Your users are at risk when a hacker is able to take control of the emails that your applications sent, but what's especially dangerous is that the emails will be coming from your company email address.
When a malicious email comes from your company email, it looks a lot more legitimate, and if your company has legitimate reasons for billing users, that makes it easier to scam unsuspecting victims into paying the hacker.
This is an issue that seems to easily go under the radar. Almost all websites, that I have supported in my 7 years of experience, were vulnerable to this. Make sure to check your websites and applications!
How to prevent HTML injection into emails #
To stop malicious users from injecting HTML into emails, you can employ the same techniques that you would use to prevent XSS:
- Don't embed user input into emails if you don't have to.
- If you have to embed user input, ALWAYS HTML-encode the user input before embedding it into emails.
- Additionally, you can detect malicious input using regular expressions or other techniques, and reject the request.
There is no bulletproof way to detect and sanitize HTML. Never solely rely on validating user input, instead always encode the user input before embedding it into code.
So how could the previous code sample be fixed? It would look something like this:
public void OnFormSubmit(string firstName, string lastName, string emailAddress) { firstName = htmlEncoder.Encode(firstName); lastName = htmlEncoder.Encode(lastName); var subject = $"{firstName}, confirm your newsletter subscription"; var htmlBody = $"Hi {firstName} {lastName}, <br />" + "Thank you for signing up for our newsletter. Please click the link below to confirm your subscription. <br />" + "<a href=\"https://some_url/confirm?token=???\">Confirm your subscription</a>"; SendEmail(subject, htmlBody, emailAddress); }
The program will HTML-encode the first name and last name before embedding it into the email. Since the email address isn't embedded into the email, it doesn't have to be encoded.
Submitting the newsletter signup form with the same malicious input will now result in the following email in your inbox:
The htmlEncoder
object in this sample is an instance of HtmlEncoder
from .NET, but there should be an equivalent API or library for your preferred programming platform.
If you're using a templating engine to generate the HTML of your emails, many of these templating engines will encode variables automatically or have built-in capabilities to encode variables.
Encoding user input with Twilio SendGrid #
Twilio SendGrid is our service for sending emails at scale and maximizing deliverability, whether those emails are sent programmatically from your application or via Marketing Campaigns. SendGrid also has your back when it comes to preventing HTML injection, because the user input will be encoded if you're using these SendGrid features:
- You can send emails with Dynamic Transactional Templates where you create the email template in SendGrid beforehand. Then, from your application, you instruct SendGrid to send the emails along with the necessary data for the template. In these templates, you can grab the passed in data using Handlebar templating. The data will be encoded if you use the default double braces
{{ }}
to embed the user input. For example, to embed the contact's first name, you can use{{ firstName }}
. If you really need to, you can output the data without encoding by using the three braces{{{ }}}
, but be careful, this can leave you vulnerable to HTML injection. - Alternatively, you can send emails using Marketing Campaigns Email Designs where you create the email template in SendGrid and send the emails to your contact list. These templates also use Handlebar templating, and the data will be encoded when using the double braces syntax. Instead of passing the data from your application, the data will come from your contact list. See the Twilio SendGrid Marketing Campaigns documentation for information about working with contacts.
If you want to send transactional emails from your application, we recommend using Dynamic Transactional Templates, but there is one more templating feature which is older and does not encode user input by default. You can send emails with Substitution Tags to embed the user input, but the user input will not be encoded for you, so make sure to encode the user input yourself!
If you send emails with SendGrid but programmatically generate the email templates yourself without using SendGrid's features to embed user input, you will have to take the same preventative measures discussed earlier.
Don't let your users get pwned via email HTML injection #
If reading this made you worried about your own applications, I've been there! Leaving your emails vulnerable to HTML injection can easily go under the radar and even vulnerability code scanners may not find it. Hopefully, after reading this post you know what to look for and how to remedy any type of injection attacks. Never trust user input and always encode it before embedding it into code.