Mailform abuse by header injection

How my feedback mailform was abused by junk mailers.

Why I need a feedback mailform

I used to offer an email address on my site until it started to get bombarded by junk mail. Even converting the email address text into an image on my old contact page — so that humans could read it but simple web spiders could not — didn't work for long. So I replaced the email address with a feedback form that a visitor could fill in with his name, email address, rough location, and a comment.

All seemed to be well until 6th February 2006. On that date, a junk mailer hiding behind multiple fake IP addresses managed to successfully send junk messages to hundreds of email addresses by abusing my feedback form. The messages either contained HTML content promoting an online pharmacy selling diet pills, or ramping shares in a Florida-based company called Strategic Growth Ventures (who issued a press release stating they had nothing to do with this junk mail campaign).

Behind the feedback form

The script that handles the information provided by the user is written in PHP. It was very simple. It just took the values provided by the user, and used the mail function to email them to my email address.

I had thought that by hardcoding my email address into the script, only I could possibly receive whatever was sent to the script. But I was wrong.

To make the setup more convenient, I had changed the script to add a "Reply-to:" header to the email message, so that when I finished reading a visitor's message I could just click Reply in my mail client and the address provided by the visitor would be ready in the reply window.

This change had introduced a huge hole in my feedback script. And this is how the junk mailer abused the script.

Abuse of the mailform script

By sending values which contained multiple lines to my script, the junk mailer was able to inject extra headers into the email which my script created. So the junk mailer was able to add "To:" headers, "CC:" headers, "Content-Type:" headers, "Subject:" headers, and "From:" headers, and more.

This all meant that the junk mailer could completely change the way the message appeared, even hiding the message body that my script creates, and have it delivered to any email addresses so desired.

This was a major problem. I can't stand junk mail, and the thought of someone using my script to harass hundreds of people infuriated me. I disabled the part of the script that sent out the compromised emails, and began to investigate how to fix the gaping hole.

Fixing the mailform script

To stop the script being abused I needed to reprogram the script to quit if it detected any multi-line values being provided to the email address or the username. So I tried the solutions suggested on several web pages about avoiding header injection, but none of them successfully rejected the values being provided by the junk mailer. In the end I opted to use a regular expression to validate the email adress value provided by the user. I used the pattern suggested by John Coggeshall on the page "Email validation with PHP 4" [no longer online].

I couldn't get this pattern to correctly block multi-line values when I was using POSIX Extended regex functions. But it seemed to work perfectly when I switched to using Perl-compatible regex functions. I think this is because Perl-compatible parsing treats the dollar-sign as meaning the end of the input string, not just any line feed character. So the pattern should only match if the entire input string is a correctly-formed email address, not if a multi-line value is provided where the contents of any line contains an email address.

So before I use the email address provided by the visitor, I check it with the following code:

$email_pattern = '/^[_a-z0-9-]+(\.[_a-z0-9-]+)*@'.
        '[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,3})$/';
if($visitor_mail != '' &&
        preg_match($email_pattern, $visitor_mail) == 0) {
    echo 'Please click Back in your browser, and check you\'ve ';
    echo 'entered your email address correctly.' ;
    $spam_warning = 'Invalid email address, was:\r\n\r\n';
    $spam_warning .= $visitor_mail ;
    mail($SEND_FEEDBACK_TO_NAME.' <'.$SEND_FEEDBACK_TO_ADDRESS.
            '>', $_SERVER['SERVER_NAME'].
            ' feedback script abuse', $spam_warning) ;
    exit(1) ;
}

If an invalid value is provided for the email address, a bare error message appears asking the user to click back in their browser and check they've entered their email address correctly. Then the script sends a "feedback script abuse" message to me (which does not use any extra headers at all). Then the script exits so that the illegal email value cannot be used in the "Reply-to:" header that the main mail function call will use.

It took me a few days to realise it, but the name provided by the visitor is also added to the "Reply-to:" header of the email (so that the user's name appears in the "To:" header of a reply message). Which means that a junk mailer could also inject headers by supplying for the name value a multi-line string that contains headers. Which meant that I also needed to enforce checking on the value provided to the script for the name value:

if ($visitor_name != '') {
    if (strlen($visitor_name) > 100 ||
            preg_match("/^[a-zA-Z0-9'_ -]+$/", $visitor_name) == 0)
            {
        echo 'Please click Back in your browser and check the ';
        echo 'name you\'ve provided. It can be up to 100 ';
        echo 'characters long, and may only contain numbers, ';
        echo 'letters, spaces, apostrophes, underscores and ' ;
        echo 'hyphens.';
        $spam_warning = 'Invalid visitor name, was:\r\n\r\n'
                .$visitor_name ;
        mail($SEND_FEEDBACK_TO_NAME.' <'.
                $SEND_FEEDBACK_TO_ADDRESS.'>',
                $_SERVER['SERVER_NAME'].' feedback script abuse',
                $spam_warning) ;
        exit(1) ;
    }
}

This is similar to the code that checks the email value. To check the provided name value, a regex is used that only permits alphanumeric characters, apostrophes, underscores, spaces, and hyphens. Any other character present in the name value will cause the user to be presented with a message asking them to go back and change the name they've provided, and then the script will mail an abuse warning to me and then exit.

Checklist for securing your own mailform script

  1. If you're already under attack, immediately comment-out every line that contains a call to the mail function which embeds any user-supplied input into a mail message. Don't re-enable it until you are very confident that you have got a secure script.
  2. Check very carefully which variables are used in any extra headers provided to the mail function. Then carefully follow those variables back and discover exactly which pieces of user input are used to create the variable used in each extra header.
  3. Any piece of input provided to the script must be checked carefully. To check an email address is correctly formed, use a regular expression such as the one above. To check that other fields are acceptable, either use a regular expression to check the value fits an acceptable pattern, or use PHP's ctype function to check that every character in the value is of a certain type. (Be careful with the return value of ctype functions, though. Their behaviour regarding empty strings has changed from version 4 to version 5.)
  4. When checking input values that will be used in a mail header, make absolutely sure that the user cannot submit values that contain line feeds or other control characters.
  5. Add a maxlength attribute to input elements in the HTML form and enforce them in your script (using the strlen function) when you check that a piece of user input is valid. A limit of 100 characters should be enough for almost anyone to enter their name. If a value longer than permitted is provided to your script, make sure the script displays an error and exits immediately.
  6. Include a line that sends a fixed mail message (containing no variable input at all) to your email address, letting you know that the script has been triggered. If you receive this fixed message but no actual feedback then something strange is going on and you need to assume you're under attack.

Paranoia

I think the script is airtight now, from a header injection point of view at least.

But if you do see any problems with my code, or in the statements I have made in this document, or if you know of any other exploits I should know about, please contact me using the new FeedBack contact page.

An apology

I'd like to apologise to the hundreds of AOL users that received an extra piece of junk email because of the exploitation of my feedback script. I hate junk mail, I don't understand who enjoys receiving it enough to keep the junk mailers sending the stuff, and I wish that western governments were able to do more to deal with the problem.