How to write secure php mail scripts

2006-01-08 5-minute read

Allowing people to fill out forms on your web site that generate email messages is an important part of any organizing web site. This feature allows you to have interested people contact you for more information, join an email list, or tell a friend about your site. PHP makes programming these types of forms a breeze.

Unfortunately, this ease of use also makes it a breeze to program these forms in ways that malicious users can take advantage of to use your web form to send email to anyone they want. Taking advantage of security holes often allows people to send advertisement to large numbers of people using your form!

Following are some PHP programming tricks which will help ensure that you don’t get used.

Whenever possible, hard code the “mail to” email address.

That means create a variable in your php code like:

$mail_to = 'your@emailaddress.org';

This technique is in contrast to placing that variable as a hidden variable in your form. When it is a hidden variable in the form, you open yourself up to malicious users manipulating the form so it posts their own “mail to” variable.

Sometimes you can’t hard code this value (like when you are creating a “tell a friend” page in which the user needs to submit the address of the user the email is being sent to). But for the forms in which the message is always sent to the same address, be sure to hard code it!

Validate all user input, especially email addresses.

Don’t trust anything the user sends you, especially email addresses.

It’s common to write forms in which a user puts in their email address and a message to send and then that message is emailed to us in such a way that is come “from” the user’s email address. Consider this code snippet:

$mail_to = 'your@emailaddress.org';
$mail_from = $_POST['mail_from'];
$subject = 'A message from a web surfer on our site';
$message = $_POST['message'];

$headers = "From: $mail_from";
mail($mail_to,$subjet,$message,$headers);

With this code, you very nicely and cleanly send a message someone posted to your site and magically it appears in your inbox coming from the user that sent it. This makes it easy to hit reply and have the reply go right to that person.

The problem is that many malicious users won’t submit just an email address in the form. They will submit an address, followed by a line break, followed by “Subject: Buy Viagra” followed by another line break followed by “To: another@emailaddress.org”, etc.

With the code snippet above, you are placing the entire unchecked contents of the mail_from field as headers in your email message! Yipes.

Avoid this problem by verifying that the from address really is a valid email address. I use the following snippet to do that:

// Define a constant that can be re-used when checking email
// addresses. If this is your first regular expression don't
// spend too much time looking at it. It may make you nauseous.
// (thanks to lightningbug.sf.net)
define(
	'EMAIL_MATCHER',
	'/^[^@\s]+\@(\[?)([-\w]+\.)+([a-zA-Z]{2,6}|[0-9]{1,3})(\]?)$/'
);
if(!preg_match(EMAIL_MATCHER,$mail_from))
{
	echo "Your email address does not appear valid.";
	exit;
}

With tell a friend emails - don’t allow users to write the body of the message.

Consider this code snippet:

$mail_to = $_POST['mail_to'];
$mail_from = $_POST['mail_from'];
$subject = "$mail_from thought you would like this site";
$message = $_POST['message'];
$headers = "From: $mail_from";
mail($mail_to,$subjet,$message,$headers);

Very nice way to allow a web surfer to send an email to their friends.

It’s also a very useful way to allow anyone in the world to send anyone else in the world a completely arbitrary message. It’s trivial to write a program that hits your web form repeatedly with a commercial message, each time with a new email address.

There are many ways around this. The method I prefer is to allow users to type their own personal message but limit it to 200 characters. Then, use a static message that they can’t change which is automatically appended to the message.

This would allow a spammer to send a 200 character message to arbitrary recipients, but the character limit is so low and the fact that your message is appended to the bottom makes it much less attractive.

Here’s an example that uses this method:

$unchangeable_message = "Change this text to the message you want ".
"to be sent with all emails sent.";
$organization = "Your organization's name";
$copy_email = "your@emailaddress.org";
$errors = array();
$email_from = $_POST['email_from'];
$name = $_POST['name'];
$email_to = $_POST['email_to'];
$name = $_POST['name'];
$brief_note = $_POST['brief_note'];

define(
	'EMAIL_MATCHER',
	'/^[^@\s]+\@(\[?)([-\w]+\.)+([a-zA-Z]{2,6}|[0-9]{1,3})(\]?)$/'
);
if(!preg_match(EMAIL_MATCHER,$email_to))
{
	$message = "The address you entered for your friend does not ".
	"appear  to be valid. You entered $email_to.";
	$errors[] = $message;
}
if(!preg_match(EMAIL_MATCHER,$email_from))
{
	$message = "The address you entered for yourself does not ".
	"appear to be valid. You entered $email_from.";
	$errors[] = $message;
}
if($name == '')
{
	$errors[] = "Please enter your name.";
}
if(!preg_match("/^[a-zA-Z ']$/",$name))
{
	$message = "Your name can only contain letters, spaces and ".
	" apostrophes.";
	$errors[] = $message;
}
if(strlen($name) > 25)
{
	$errors[] = "Your name must be less than 25 characters.";
}
if(strlen($brief_note) > 200)
{
	$message = "You can only enter 200 characters for your note. " .
	"Sorry - this restriction is to prevent spammers from ".
	"abusing this form!";
	$errors[] = $message;
}
if(count($errors) > 0)
{
	echo "Thanks for your submission, however, we encountered the ";
	echo "following errors! Please hit back on your browser and try ";
	echo "again.";
	echo implode('<br>',$errors);
}
else
{
	$subject = "$name thought you'd be interested in $organization";
	$body = wordwrap($brief_note) . "\r\n\r\n--------\r\n\r\n" .
	wordwrap($unchangeable_message);
	$headers = "From: $email_from\r\nCc: $copy_email";
	if(mail($email_to,$subject,$body,$headers))
	{
		echo "Thanks for passing on the word about $organization";
	}
	else
	{
		echo "Woops. We hit a bug sending to $email_to. Please contact";
		echo "support.";
	}
}

Monitor

Be sure to watch your log files to make sure nobody is abusing your forms. If you get strange emails sent by your form, forward them to support to make sure it’s not a sign of abuse.