The Host
header in an HTTP request is set by the browser and can be used by backend servers to distinguish requests
from the different domains being served on the same internet protocol address. However, if a web-server relies on the
supplied value of the Host header, a malicious user can provide a spoofed value to generate misleading links on your
website and in transactional emails.
Risks
Discussion
Most web applications do not know what domain they are being hosted on by default. Domain names are registered in the Domain Name System, which tells a browser to route internet traffic for a domain to a number of IP addresses. HTTP and HTTPS traffic sent to these IP addresses will be routed to a load-balancer, which then distributes the requests amongst various web-servers. The running application will sit downstream of this, unaware of how the requests entered the network, passing responses back along the same route.
Browsers set a Host
header in each HTTP request, indicating the intended domain of the HTTP request.
However, there’s generally no way to check whether the domain in the Host
header corresponds to the IP address the
underlying TCP handshake was initiated with. A malicious user-agent - like a script run by a hacker - can set the
Host
header to anything they choose.
Relative vs Absolute URLs
Most of the time a website doesn’t actually need to know what domain it is running under, providing links in HTML are relative URLs, and client-side imports are reference relative URLs in the same manner:
<!-- Links within the site don't need the domain specified. -->
<a href="/profile">Profile</a>
<!-- Local imports don't need the domain specified either. -->
<script src="js/navigation.js"/>
Using relative URLs is best practice in web development: they make it easier to deploy web-code under different environments. However, there are some circumstances when absolute URLs are required, such as when generating website links in transactional emails. If a user requests a password reset email, for example, the link in the email needs to contain the full domain of your website, since the user will be navigating from an external source (the email client).
Generating Absolute URLs
In the password reset scenario, it is tempting to simply take the domain from the Host
header. However, remember this
can be set to any value an attacker chooses, so the following functions are open to abuse:
Python
import smtplib
from email.message import EmailMessage
def send_password_reset_email(request, user):
message = EmailMessage()
# Trusting the "Host" header from the request is not safe. Don't do this!
password_reset_url = 'https://{}/password/reset/{}'.format(
request.headers['Host'], user.reset_token)
message.set_content("Click here to reset your password: " + password_reset_url)
message['Subject'] = "Forget your password?",
message['To'] = user.email
message['From'] = "support@example.com",
smtp = smtplib.SMTP(os.environ.get("SMTP_HOST"))
smtp.send_message(message)
smtp.quit()
Node
const nodemailer = require('nodemailer')
async function sendPasswordResetEmail(request, user) {
let transporter = nodemailer.createTransport({
host : process.env.SMTP_HOST,
port : 465,
secure : true,
auth: {
user : process.env.SMTP_USER,
pass : process.env.SMTP_PASSWORD
}
})
// Trusting the "Host" header from the request is not safe. Don't do this!
const passwordResetURL = `https://${request.headers.host}/password/reset/${user.reset_token}`
let info = await transporter.sendMail({
from: 'support@example.com',
to: user.email,
subject: 'Forget your password?',
text: `Click here to reset your password: ${passwordResetURL}`,
html: `<a href="${passwordResetURL}">Click here to reset your password</a>`
})
console.log('Password reset sent: %s', info.messageId)
}
Java
public static void sendPasswordResetEmail(HttpServletRequest request,
String toAddress,
String passwordResetToken) throws MessagingException
{
// Trusting the "Host" header from the request is not safe. Don't do this!
String passwordResetURL = "https://" + request.getHeader("Host")
+ "/password/reset/"
+ passwordResetToken;
Properties properties = System.getProperties();
Session session = Session.getDefaultInstance(properties);
MimeMessage message = new MimeMessage(session);
message.setFrom(new InternetAddress("support@example.com"));
message.addRecipient(Message.RecipientType.TO, new InternetAddress(toAddress));
message.setSubject("Forget your password?");
message.setText("Click here to reset your password: " + passwordResetURL);
Transport.send(message);
}
C#
public async void SendPasswordResetEmail(IdentityUser user)
{
var resetToken = await userManager.GeneratePasswordResetTokenAsync(user);
// Trusting the "Host" header from the request is not safe. Don't do this!
var passwordResetUrl = $"https://:{Request.Host}/Account/ResetPassword?userId=${user.Id}&code=${resetToken}";
var text = $"Please click on this link to reset your password: {passwordResetUrl}";
MailMessage msg = new MailMessage();
msg.From = new MailAddress("support@example.com");
msg.Subject = "Forget your password?";
msg.To.Add(new MailAddress(user.Email));
msg.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(text, null, MediaTypeNames.Text.Plain));
SmtpClient smtpClient = new SmtpClient(Configuration.EmailHost, Convert.ToInt32(Configuration.EmailPort));
smtpClient.Send(msg);
}
Safely Generating Absolute URLs
An attacker could abuse these functions to send a password reset email to a victim with a link back to a malicious website under their control. If this site looks confusingly similar to yours - and has a similar enough domain name that the victim does not notice - the attacker has an easy way to harvest credentials.
For this reason you should always take the domain name for absolute URLs from a configuration file stored securely on the server. Below are some ways to safely generate password reset emails:
Python
import smtplib
from email.message import EmailMessage
def send_password_reset_email(user):
message = EmailMessage()
# Taking the Host from the configuration parameters is safe.
password_reset_url = 'https://{}/password/reset/{}'.format(
os.environ.get("DOMAIN"), user.reset_token)
message.set_content("Click here to reset your password: " + password_reset_url)
message['Subject'] = "Forget your password?",
message['To'] = user.email
message['From'] = "support@example.com",
smtp = smtplib.SMTP(os.environ.get("SMTP_HOST"))
smtp.send_message(message)
smtp.quit()
Node
const nodemailer = require('nodemailer')
async function sendPasswordResetEmail(request, user) {
let transporter = nodemailer.createTransport({
host : process.env.SMTP_HOST,
port : 465,
secure : true,
auth: {
user : process.env.SMTP_USER,
pass : process.env.SMTP_PASSWORD
}
})
// Taking the Host from the configuration parameters is safe.
const passwordResetURL = `https://${process.env.HOST}/password/reset/${user.reset_token}`
let info = await transporter.sendMail({
from: 'support@example.com',
to: user.email,
subject: 'Forget your password?',
text: `Click here to reset your password: ${passwordResetURL}`,
html: `<a href="${passwordResetURL}">Click here to reset your password</a>`
})
console.log('Password reset sent: %s', info.messageId)
}
Java
public static void sendPasswordResetEmail(String toAddress, String passwordResetToken) throws MessagingException
{
Properties properties = System.getProperties();
// Taking the Host from the configuration parameters is safe.
String passwordResetURL = "https://" + properties.getProperty("host")
+ "/password/reset/"
+ passwordResetToken;
Session session = Session.getDefaultInstance(properties);
MimeMessage message = new MimeMessage(session);
message.setFrom(new InternetAddress("support@example.com"));
message.addRecipient(Message.RecipientType.TO, new InternetAddress(toAddress));
message.setSubject("Forget your password?");
message.setText("Click here to reset your password: " + passwordResetURL);
Transport.send(message);
}
C#
public async void SendPasswordResetEmail(IdentityUser user)
{
var resetToken = await userManager.GeneratePasswordResetTokenAsync(user);
// Taking the Host from the configuration parameters is safe.
var passwordResetUrl = Url.Action(
"ResetPassword", "Account",
new { UserId = user.Id, code = resetToken },
protocol: Request.Scheme);
var text = $"Please click on this link to reset your password: {passwordResetUrl}";
MailMessage msg = new MailMessage();
msg.From = new MailAddress("support@example.com");
msg.Subject = "Forget your password?";
msg.To.Add(new MailAddress(user.Email));
msg.AlternateViews.Add(AlternateView.CreateAlternateViewFromString(text, null, MediaTypeNames.Text.Plain));
SmtpClient smtpClient = new SmtpClient(Configuration.EmailHost, Convert.ToInt32(Configuration.EmailPort));
smtpClient.Send(msg);
}
Mitigation
In summary, you can protect your site against Host
header poisoning by:
- Using relative URLs wherever possible.
- Where absolute URLs are required - like in transactional emails - take the domain name from server-side configuration.