Anyone who has an email address can tell you that spam is one of the great banes of the online world. But it's not only distant servers owned by the spammers that are to blame. It may even be your very own server. Insecure PHP scripts have provided great opportunities for spammers to abuse other's resources to send out their spam. In particular, it's the mail() function that can be abused. I myself was the target a few months ago when I noticed spam being sent from an old form on my server that I'd forgotten about. This month's article looks at techniques that can be used to harden your mail form, and reduce the chances of it being misused.

How do the spammers find you?

There are two main ways they find you. One of course, is by visiting your site, and following the links. If you have a publicly available contact form, there's not much you can do about this one. The other, surprisingly common, way is through a search engine such as Google. By googling for mail.php, contact.php, and the like, spammers will get a list of likely targets. You can reduce this likelihood by choosing an unusual name. This sort of security by obscurity is certainly not anywhere near sufficient, but I'm a fan as, all else being equal, the less attempts, the less chance of an exploit. You of course can't stop people googling for words such as contact on your web page, which should then lead to your script, but for spammers googling for mail.php and so on it is preferable, as with this one effort, they are able to identify a mail form and a PHP script--and they're fairly certain that the developer hasn't read this article!
Here's a simple HTML form used for sending mail, followed by the script for sending it. It's stripped to its bare minimum for now.
form.html (1)
<form action='mail.php' method='post'>
Email: <input type='text' name='email'><br>
Mail body: <textarea name='body'></textarea><br>
<input type='submit' value='Send comments'>
</form>

mail.php (1)

<?php
$to      = "bob@domain_example.co.za";
$subject = "Email from website";
$message = $_REQUEST["body"];
$email = $_REQUEST["email"];

$headers = "From: $email";
mail($to, $subject, $message, $headers);
echo "Thanks for submitting.";

?>
Here's what the results should look like (you can view the message source on your email client to see, with the exception of any bcc field, exactly what was sent).
To: bob@domain_example.co.za
Subject: Email from website
From: sender@their_domain.co.za
However, this form and script combination is a spam relay waiting to happen. Notice that the to address is hard-coded. Many applications allow the user to specify the to address, and this is of course opens the door even further. However, too many people naively assume that hard-coding it is sufficient to avoid spam. This is certainly not true. The script above uses $_REQUEST, which accepts both $_POST and $_GET variables. This is simply to ease the exploit even further for demonstration purposes. It's best to specify $_POST (in the case of our form's example), but limiting a potential spammer to $_GET or $_POST will at best cause an insignificant delay.

Exploiting the script

Assuming the above form and script sit on a server called your-domain.co.za, spammers can simply send the following request:
http://your-domain.co.za/mail.php?body=gotcha&email=barbie@fake-domain.com%0Abcc:spam-1@some-domain.com,spam2@some-domain.com
Let's look at this in more detail. The variable names are available in the form, so the spammer knows to use body and email. The body will contain the contents of the mail (in this case just the single word, gotcha), though of course in practice it would be cheap deals and viagra and the offers of millions if you just supply your bank account details to the ex-wife of some or other dictator.
Next comes the the sneaky bit. The email field, which supposedly just contains the sender's email (in this case barbie@fake-domain.com is used), also contains a bcc field, followed by a comma-delimited list of emails to be spammed. The %0A character is a linefeed.
Here's what the email headers would look like.
To: bob@domain_example.co.za
Subject: Email from website
From: barbie@fake-domain.com
Bcc: spam-1@some-domain.com,spam2@some-domain.com
Without any protection, it's a fairly simple matter for a mail form to be abused. So how can we protect against this? There are a number of things to do, and a number of spammer responses, so I suggest reading through the whole article carefully before making any changes.

Checking for valid email

Since the email field was used to mask the bcc list, and it makes good sense to avoid receiving comments from people who don't bother to put a valid email in, one way to stop this is to test for a valid email. This is not enough, but it's a step closer towards the principle of not trusting any values that the user can edit. The changes are in bold:

mail.php (2)

<?php
$to      = "bob@domain_example.co.za";
$subject = "Email from website";
$message = $_REQUEST["body"];
$email = $_REQUEST["email"];

function is_valid_email($email) {
  return preg_match('#^[a-z0-9.!\#$%&\'*+-/=?^_`{|}~]+@([0-9.]+|([^\s]+\.+[a-z]{2,6}))$#si', $email);
}

if (!is_valid_email($email)) {
  echo 'Sorry, invalid email';
  exit;
}

$headers = "From: $email";
mail($to, $subject, $message, $headers);
echo "Thanks for submitting.";
?>
It's too soon to be complacent. Let's change the example slightly by adding a subject field:

form.html (2)

<form action='mail.php' method='post'>
Email: <input type='text' name='from_email'><br>
Subject: <input type='text' name='subject'><br>
Mail body: <textarea name='body'></textarea><br>
<input type='submit' value='Send comments'>
</form>
mail.php (3)
<?php
$to      = "bob@domain_example.co.za";
$subject = "Email from website";
$message = $_REQUEST["body"];
$subject = $_REQUEST["subject"];
$email = $_REQUEST["email"];

function is_valid_email($email) {
  return preg_match('#^[a-z0-9.!\#$%&\'*+-/=?^_`{|}~]+@([0-9.]+|([^\s]+\.+[a-z]{2,6}))$#si', $email);
}

if (!is_valid_email($email)) {
  echo 'Sorry, invalid email';
  exit;
}

$headers = "From: $email";
mail($to, $subject, $message, $headers);
echo "Thanks for submitting.";
?>
Once again this can be exploited, with something like:
http://domain_example.co.za/tests/mail3.php?body=gotcha&email=barbie@fake-domain.com&subject=GETVIAGRA%0Acc:spam-1@some-domain.com,spam2@some-domain.com

How to avoid all of this spam

It's not just bcc that should worry us. There are other strings that spammers can use. These include: content-type:, mime-version:, multipart/mixed, cc as well as bcc. Spammers prefer bcc, but they don't always mind if everyone sees the full list of addresses they've spammed. I'm not going to focus on demonstrating all the possible exploits, rather, I'll focus on preventing the exploits. Suffice to say there are all sorts of possibilities, including sending HTML attachments and the like. This next addition checks for the existence of certain suspicious strings in any of the submitted values. If they exist, the mail is not sent. Similarly, the existence of newline characters is usually an indication of something unusual going on.

mail.php (4)

<?php
$to      = "bob@domain_example.co.za";
$subject = $_REQUEST["subject"];
$body = $_REQUEST["body"];
$email = $_REQUEST["email"];

function is_valid_email($email) {
  return preg_match('#^[a-z0-9.!\#$%&\'*+-/=?^_`{|}~]+@([0-9.]+|([^\s]+\.+[a-z]{2,6}))$#si', $email);
}

function contains_bad_str($str_to_test) {
  $bad_strings = array(
                "content-type:"
                ,"mime-version:"
                ,"multipart/mixed"
		,"Content-Transfer-Encoding:"
                ,"bcc:"
		,"cc:"
		,"to:"
  );
  
  foreach($bad_strings as $bad_string) {
    if(eregi($bad_string, strtolower($str_to_test))) {
      echo "$bad_string found. Suspected injection attempt - mail not being sent.";
      exit;
    }
  }
}

function contains_newlines($str_to_test) {
   if(preg_match("/(%0A|%0D|\\n+|\\r+)/i", $str_to_test) != 0) {
     echo "newline found in $str_to_test. Suspected injection attempt - mail not being sent.";
     exit;
   }
} 

if (!is_valid_email($email)) {
  echo 'Invalid email submitted - mail not being sent.';
  exit;
}

contains_bad_str($email);
contains_bad_str($subject);
contains_bad_str(body);

contains_newlines($email);
contains_newlines($subject);

$headers = "From: $email";
mail($to, $subject, $body, $headers);
echo "Thanks for submitting.";
?>

Further hardening

I'm partly hesitant to discuss the next part, as these additions are not strictly speaking necessary. However, I'm in favour of doing them in principle, and they're worth mentioning to get developers thinking along those sorts of lines. They have uses beyond just mail injection. They certainly don't provide a foolproof technique to avoid abuse. They involve doing what you can to ensure that a script is only accessed in the right context. If it should only ever be called as part of a POST procedure, ensure that it tests for this and doesn't permit access if it's anything else. Here's the script with this addition.

mail.php (5)

<?php
$to      = "bob@domain_example.co.za";
$subject = $_REQUEST["subject"];
$body = $_REQUEST["body"];
$email = $_REQUEST["email"];

$dodgy_strings = array(
                "content-type:"
                ,"mime-version:"
                ,"multipart/mixed"
                ,"bcc:"
);

function is_valid_email($email) {
  return preg_match('#^[a-z0-9.!\#$%&\'*+-/=?^_`{|}~]+@([0-9.]+|([^\s]+\.+[a-z]{2,6}))$#si', $email);
}

function contains_bad_str($str_to_test) {
  $bad_strings = array(
                "content-type:"
                ,"mime-version:"
                ,"multipart/mixed"
		,"Content-Transfer-Encoding:"
                ,"bcc:"
		,"cc:"
		,"to:"
  );
  
  foreach($bad_strings as $bad_string) {
    if(eregi($bad_string, strtolower($str_to_test))) {
      echo "$bad_string found. Suspected injection attempt - mail not being sent.";
      exit;
    }
  }
}

function contains_newlines($str_to_test) {
   if(preg_match("/(%0A|%0D|\\n+|\\r+)/i", $str_to_test) != 0) {
     echo "newline found in $str_to_test. Suspected injection attempt - mail not being sent.";
     exit;
   }
} 

if($_SERVER['REQUEST_METHOD'] != "POST"){
   echo("Unauthorized attempt to access page.");
   exit;
}

if (!is_valid_email($email)) {
  echo 'Invalid email submitted - mail not being sent.';
  exit;
}

contains_bad_str($email);
contains_bad_str($subject);
contains_bad_str(body);

contains_newlines($email);
contains_newlines($subject);

$headers = "From: $email";
mail($to, $subject, $body, $headers);
echo "Thanks for submitting.";
?>
Similarly, if your mail script should only ever be called from a particular page, check to see if the user is coming from the right referer. Any attempt to directly access the page, or call it from any other page, should also result in an error, with further access denied. This sort of thing can be done with PHP, but is perhaps best done through the web server itself. However, the referer can also be faked. A further suggestion, which I've never implemented myself but makes a lot of sense, is to do some form of IP checking. If the IP has not visited the specific page shortly prior to calling the script, deny access. This avoids the problem of using a fake referer and going directly to the mail script.

Conclusion

Of course there are all sorts of things that you can do to enhance this script. You may want to log spam attempts either to a file or database. Useful data to capture includes the IP, referrer, user-agent, request string and request method. The request string can provide great insight into the minds of the spammers, as there will usually be quite a few attempts from the same source. The effort is worth it for the spammer, as they're putting in the work hoping to get a long-term benefit - a script that they can continue to exploit for a long time. Implement these changes, and your scripts should be a lot more secure - but don't sit back after that. Keep your eyes open. Security needs constant attention! Good luck.