How to prevent email HTML injection in C# and .NET

March 21, 2022
Written by
Reviewed by

How to prevent email HTML injection in C# and .NET

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 editions, and that security issue is vulnerability to injection attacks. I previously talked about injection attacks in general and more specifically, how dangerous email HTML injection attacks are and how you can prevent them. However, in this post, you'll learn how you can mitigate HTML injection attacks in .NET specifically.

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 Cross Site Scripting (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.

A vulnerable ASP.NET example


This example uses ASP.NET Razor Pages, but the same vulnerabilities can occur in Web Forms, MVC, Minimal API, Blazor, or even client-side .NET applications.

Consider this ASP.NET web application where a user can fill out a form to sign up for a newsletter. The form to sign up for the newsletter is rendered using the following Razor code:

@page
@model IndexModel
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
<form method="post">
    <label for="firstName">First name</label>
    <input asp-for="SignUpForm.FirstName" id="firstName" required> <br>
    <label for="lastName">Last name</label>
    <input asp-for="SignUpForm.LastName" id="lastName" required> <br>
    <label for="email">Email Address</label>
    <input asp-for="SignUpForm.EmailAddress" type="email" id="email" required> <br>
    <button type="submit">Sign Up</button>
</form>

The output will be a form that prompts the user for their first name, last name, and email address.

A form with a first name field, a last name field, an email address field, and a signup button

When the user submits the form, the HTTP POST request will be handled by the OnPostAsync method in the code-behind of the Razor page:

using System.ComponentModel.DataAnnotations;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace HtmlEmailInjection.Pages;

public class IndexModel : PageModel
{
    // extra code omitted for brevity

    [BindProperty] public SignUpForm SignUpForm { get; set; }

    public async Task<IActionResult> OnPostAsync([FromServices] EmailSender emailSender)
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        var firstName = SignUpForm.FirstName;
        var lastName = SignUpForm.LastName;
        var emailAddress = SignUpForm.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://localhost/confirm?token=???\">Confirm your subscription</a>";

        await emailSender.SendEmailAsync(subject, htmlBody, emailAddress);

        return new ContentResult {Content = "Thank you for signing up!"};
    }
}

public class SignUpForm
{
    [Required] public string FirstName { get; set; }

    [Required] public string LastName { get; set; }

    [Required, EmailAddress] public string EmailAddress { get; set; }
}

MVC binds the form values to the SignUpForm property which is used in the OnPostAsync method.
The OnPostAsync checks if the form is valid, if so, it generates a string for the email subject and the email body by embedding the user input.

The email is then sent with the generated subject and body to the email address provided by the user, and the web server responds with "Thank you for signing up!" to the user.

If a user signs up for the newsletter, this is the resulting email:

Legitimate newsletter signup email with a link to confirm subscription viewed in the macOS Mail app.

However, if a malicious user enters HTML into the form, the email could look like this:

The macOS Mail app viewing a malicious email that modified the email content to say "Your bill is due, Pay now or we will charge a late fee." including a link to the scammer"s website.

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.

The Gmail app viewing a malicious email that modified the email content to say "Your bill is due, Pay now or we will charge a late fee." including a link to the scammer"s website. The malicious content is intermingled with the legitimate newsletter signup content.

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 send, 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 example be fixed? Take a look at this updated OnPostAsync method:

public async Task<IActionResult> OnPostAsync(
    [FromServices] EmailSender emailSender,
    [FromServices] HtmlEncoder htmlEncoder
)
{
    if (!ModelState.IsValid)
    {
        return Page();
    }

    var firstName = htmlEncoder.Encode(SignUpForm.FirstName);
    var lastName = htmlEncoder.Encode(SignUpForm.LastName);
    var emailAddress = SignUpForm.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://localhost/confirm?token=???\">Confirm your subscription</a>";

    await emailSender.SendEmailAsync(subject, htmlBody, emailAddress);

    return new ContentResult {Content = "Thank you for signing up!"};
}

In addition to injecting the emailSender parameter using Dependency Injection, a new htmlEncoder of type HtmlEncoder is injected into the OnPostAsync method. The HtmlEncoder-class contains methods to encode and decode HTML. Before embedding the user input into the htmlBody string, the first name and last name are HTML-encoded using the htmlEncoder.Encode method. Then, the email is sent as before and the server responds with "Thank you for signing up!".

Submitting the newsletter signup form with the same malicious input will now result in the following email in your inbox:

The macOS Mail app viewing the newsletter signup email with malicious HTML that has been HTML-encoded. The HTML code itself is shown instead of rendering it.

As you can see, the HTML is no longer rendered by the mail client, and instead shown as plain text. As a result the email looks very suspicious and hopefully will not succeed into tricking users.

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. In ASP.NET, Razor will automatically HTML-encode variables when rendering the template. The only way to not encode variables in Razor, is to use Html.Raw which is why you should only use it if you absolutely need to and use it with a lot of caution!

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:

  1. 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.
  2. 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 also send emails with Substitution Tags to embed the user input, however 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 in .NET. Never trust user input and always encode it before embedding it into code.

Niels Swimberghe is a Belgian American software engineer and technical content creator at Twilio. Get in touch with Niels on Twitter @RealSwimburger and follow Niels’ personal blog on .NET, Azure, and web development at swimburger.net.