Skip to Main Content
August 31, 2023

Crafting Emails with HTML Injection

Written by Luke Bremer

Have you ever wanted to send an email from a domain you don’t have SMTP credentials for? With some HTML injection, we may be able to do just that.

From time to time, applications have a need to notify users that an action has occurred or that something in the application needs attention. This may come in the form of a notification icon or an email to the user. In modern applications, most emails support HTML, which is often enabled by default. This gives developers a way to include the company logo or a product image in the email along with easy-to-read links for users.

When securing an application, it's common to HTML-encode user input to prevent client-side vulnerabilities such as XSS. What is less common is encoding in emails that contain HTML. If an HTML-enabled email is sent that contains user input, it can be vulnerable to HTML injection, which can allow an attacker to change the entire body of an email to something the attacker controls and can contain links to malicious sites.

Common places that can allow such behavior are sections of an application in which a user can send invites to other users or notify a user when an action is complete. The injected values can vary depending on the context of the email. An example is an email to a colleague that allows the recipient access to a section of an application. The email might contain the first name and email address of the sender along with the recipient’s email address. It may even allow the sender to craft the email with a markdown editor.

Figure 1 - Markdown Email Editor

The application then sends an email using an HTTP POST request. The body of the request can contain parameters used in the email.

Figure 2 - HTTP Request Body of Email Message

On the server side of the application, the email may go through an additional audit to ensure that the links added are only for the current domain or the application may add the company image as a header with an unsubscribe link at the bottom.

The issue with not encoding user input in an email is that any unencoded input can allow the entire body of the email to be altered. It doesn’t matter if the input is a name at the start of the email or a signature at the bottom. HTML allows style tags to be set that can be applied globally.

For instance, let’s say we send an email without encoding the current user’s name.

Figure 3 - Email HTML

If my name is 'User' then everything looks fine and the email output on the right is what is expected. But let’s say I just changed my name to:

</p><style>p{display:none;}</style><span>This is now the only thing that shows up.</span><p>

I know it’s hard to pronounce but I just felt like it suited me. Now when we render the HTML the original message is hidden, and the only thing left is our message.

Figure 4 - Updated Name in Email HTML

So, if the application adds a footer that the user cannot control, the original footer can still be hidden and replaced with your own.

In HTML, you can often add the start of an HTML comment to the end of a payload, and anything after that is marked as a comment and not processed. Ending a payload with <!— can hide the original footer and allows us to replace it with our own.

Figure 5 - HTML Comment to Hide Email Footer
Figure 6 - Attacker-Controlled Email Footer

Sometimes it can be hard to know what elements are being used in an email to determine which HTML tag to break out of. Instead of trial and error, you can view the source of the email, which shows the HTML being used. In Outlook, this can be done by right-clicking an email in the inbox and selecting 'View Source,' or in Gmail by clicking the more option icon (three (3) vertical dots) when viewing an email and selecting 'Show Original.'

Depending on how the email was sent and what provider is used by the recipient, the HTML may be in a parameter as a Base64 encoded value. 

Figure 7 - Base64 Encoded Email HTML

If the email is encoded, you can base64 decode the value and put it in a text editor to see where the values you control are shown in the HTML. An easy way to find your input is to use a canary string with a little HTML to confirm the functionality is vulnerable. If your first name is showing in an email, change your name to testqwerty<b>12345</b> and search for testqwerty in the email source to find where the value is added to the HTML. This is similar to injecting an XSS payload, but here we cannot execute JavaScript. If the bold tags in the canary string are unencoded in the email source, then you have HTML injection.

What if we don’t have any user input to edit? Consider something like the forgot password functionality, where the only input we have is the email address of the account. If we inject some HTML into the email parameter, the application won’t send the email because it is not in the correct format.

If you have spent any time looking at application requests, you know there are lots of values that are user controlled other than URL parameters or body parameters. Depending on how the application functions, it may use values stored in request headers, such as the host header or a cookie value. Additionally, some servers will use request headers to determine information about the user, such as the user agent or X-Headers. A header that is commonly used to obtain a user’s IP address is the X-Forwarded-For header. Typically, this is used when a request is proxied so that the server knows where the request originated.

Going back to the forgot password example, let’s say that when a password reset is issued, the user is sent an email with a URL to reset their password. That email may contain something along the lines of:

“A password reset request was issued from California with a source IP address of If this was not you, please contact us using the phone number below. If this was intentional, use the link below to reset your password.”

If that email is using request headers, such as the X-Forwarded-For header to set the value of, then you may be able to send other users an email with a body that you control.

Figure 8 - HTML Injection in Request Header

Because the header is being entered into the email body (as the IP address), we can break out of the tag the IP address is in. In this case, the IP address is in a span tag inside a few tables. We can break out of the tag with </span> and repeat </td></tr></table> until we are at the HTML root. Now we can add a new div tag with whatever we want the email body to be and add a style tag to hide all the tables in the HTML. We can also end our payload with a starting comment to remove any footer element that may not be inside a table or left over because of missing tags.

When the email renders, all the original content that was stored in HTML tables is now hidden and our div tag is all that remains.

Figure 9 - Altered Email Body With HTML Injection

Additionally, if the email was using the host header as the domain to create the password reset link, we may be able to change our host header to a domain that we control.

Figure 10 - Host Header Injection

The request is still sent to the original application domain but the link in the recipient’s email may look something like:

If a user then clicks the password reset link in the email, a request will be made to a server we control, and the request will likely have the reset token needed to reset that user’s password.

With email injection, any messages you inject will come from the original sender, and if the subject of the email is vague or user controlled, it can be difficult to know that the email has been altered as it's coming from a trusted source.

The remediation for this is to HTML encode any user input added to emails. Or if you do not need to use HTML in your emails, then ensure your email functions have HTML disabled. For instance, the SmtpClient Class in .NET allows you to set the IsBodyHtml parameter to false.

As stated previously, this is not XSS, and JavaScript will not run when injected into an email. But any HTML in the body of the email can be changed, and external images can be loaded. At a minimum, an attacker can reveal the IP address of a victim if that victim has external images set to automatically load.

In the end, output encoding is still our friend and will serve you well—as long as you know where your user input is.