Email Header Injection in PHP

February 24th, 2006

It has become apparent that spammers are getting even smarter than we take them for. Rather than abuse open relays, which are almost non-existent, these scum-of-the-earth 'traders' are now abusing contact forms.

However, the problem isn't necessarily that they are attempting to use the contact forms. The problem that exists is that the average coder with little or no understanding of security issues can create a contact form for their website which can act as an open invitation to spammers.

The Problem

Lets take a quick look at a sample of code:

PHP:
  1. $to = 'contact@domain.com';
  2.   $subject = $_POST["subject"];
  3.   $message = $_POST["message"];
  4.   $headers = "From: ".$_POST["from"];
  5.   mail($to,$subject,$message,$headers);

A simple enough script you might think. It takes in the user's input, and then sends the email using PHP's mail() function. As we've already defined the 'To' field, surely we're safe from it being sent to someone else? This is where people get caught up. What happens in the above code is that the user supplies their own email address, which is then placed in the From: header, and possibly a Reply-To: header as well. An unscrupolous user can easily insert other headers into the $_POST["from"] variable which are then inserted into the email, and is then well on your way to abusing this mail script and getting it to send the email to multiple recipients. Just how possible is this? One way is to create a separate form and submit it to your mailer script:

<form action="http://www.yourdomain.com/mail.php" method="POST">
To: <textarea name="to"></textarea><br />
Subject: <textarea name="subject"></textarea><br />
From: <textarea name="from"></textarea><br />
<textarea name="message"></textarea><br />
<input type="submit" value="Send">
</form>

Entering the following to the From textarea will result in it sending the email to an unsuspecting recpient:

test@domain.com
Bcc: another@user.com

Simple really. Additional BCC addresses can be separated by commas. You could check for the HTTP Referrer on the mail.php script, but that is way too easy to forge. The correct way to overcome this is to correctly validate your input, ensuring that what was entered is exactly what you want.

Validate, Validate, Validate

Input validation is very easy to do, and is the one thing that should always be in the fore-front of your mind. For the contact form, the correct thing to do is to validate all your input. I found a fairly decent validation routine on the PHP website:

PHP:
  1. /**
  2. * Check single-line inputs:
  3. * Returns false if text contains newline character
  4. */
  5. function has_no_newlines($text)
  6. {
  7.    return preg_match("/(%0A|%0D|\n+|\r+)/i", $text);
  8. }
  9.  
  10. /**
  11. * Check multi-line inputs:
  12. * Returns false if text contains newline followed by
  13. * email-header specific string
  14. */
  15. function has_no_emailheaders($text)
  16. {
  17.    return preg_match("/(%0A|%0D|\n+|\r+)(content-type:|to:|cc:|bcc:)/i", $text);
  18. }

We can further validate our data and check that fields that are used to enter Email Addresses only contain valid characters. A simple google search for email regex patterns will bring up a wealth of information. RegexpLib is a good site to use as a base, however, they usually require minimal modification to work with preg_match(). Here's a basic regexp by Rob Eberhardt to check an email address:

PHP:
  1. if(!preg_match("/^[A-Z0-9._%-]+@[A-Z0-9.-]+.[A-Z]{2,4}$/i",$_POST["from"])) {
  2. //email address is invalid
  3. die("Invalid Email");
  4. }

And there we have some basic input validation designed to avoid email header injection. You might think 'Well, what's the point?' In real life, if your contact form script gets abused by spammers, chances are your hosts will suspend and/or terminate your account, as once it starts sending out thousands of emails, it can quickly bring a server to a halt. Furthermore, the IP of that server can get blacklisted resulting in regular emails being blocked. So the sensible thing to do would be to validate your input, and save everyoone the hassle of dealing with spammers.


 Add to del.icio.us    Digg this    Technorati

Related Posts:

Entry Filed under: PHP, Input Validation

50 Comments Add your own

  • 1. Chris H  |  February 24th, 2006 at 2:52 pm

    Jee, is the problem solved if you just hard-code the from address into the mailng code so it's not sent when the form data is submitted to the mailing code?

  • 2. Barnaby Knowles  |  February 24th, 2006 at 2:56 pm

    I only started validating my contact forms fairly recently when I found that attempts were being made to abuse of one them. Within a few weeks of learning about this type of attack all of my forms were having attempts to hijack them. New tricks must spread fast amongst spammers!

    Thanks for the thorough code samples, my methods are not as concise yet! I would also suggest checking for Mime-Version: in the has_no_emailheaders() function as well as content-type:.

  • 3. Nick  |  February 24th, 2006 at 2:59 pm

    Thank you for the well-timed article. Because of my inexperience when first creating my contact form, I didn't bother with validation and I just started getting hit with email spamming attacks using my contact form.

    Chris H -
    No the problem is not solved in that manner. Evidently the header's information can be repeated time and time again. Bummer, huh?

  • 4. Khalid  |  February 24th, 2006 at 3:04 pm

    I should have mentioned that the above functions aren't the be all and end all, but they should start you out on the path to securing your scripts.

    If you hardcode all your values apart from say the body variable, then I believe there shouldn't be an issue. The body is placed after the headers in an email, and as such, it won't add to them. However, you should make sure that you check that for misguided information as well. Regular Expressions are your friends!

    I've found this page to be a good resource for having a further look. Furthermore, in addition to Mime-Version: and Content-Type:, there are several other headers to check for as well. The PHP mailing list has a few posts on this, and I found a useful function in one post on email injection that is a good read.

    Khalid

  • 5. Nate K  |  February 24th, 2006 at 3:07 pm

    Not that this is anything new - spammers have been using this for quite some time now. You give some good validation examples, for some more information on securing your forms with tokens, encryption, and salt check out Chris Shiflett's blog (www.shiflett.org).

    Good post and I hope that others take the time to implement some of these basic things!

    Peace,
    Nate

  • 6. Tony Smith  |  February 24th, 2006 at 3:34 pm

    Silly (beginner's) question - how might you impliment this over a whole page?

    I appreciate the validation points, but how do you correctly apply them to each part of your code? Can you put the addtional lines in above that and call your form in the usual style?

  • 7. Khalid  |  February 24th, 2006 at 3:44 pm

    Well a quick and brutal way would be to do something like this:

    foreach($_POST as $key=>$value) {
    $_POST[$key] = (!has_no_emailheaders($value)) ? '' : $value;
    }
    ?>

    What that does is run through the entire $_POST array, and checks each value against the has_no_emailheaders() function. If a header is found, then the variable is cleared.

  • 8. Barnaby Knowles  |  February 24th, 2006 at 3:46 pm

    Tony, you validate the user's input after the form has been submitted. Call your form as usual, and then once it has been submitted you validate the input with the code above, and then if all is OK, send your email.

    Hope that makes sense. :-)

  • 9. Andy Downie  |  February 24th, 2006 at 3:57 pm

    Well I'm even simpler than Tony.
    I find the info all very useful, and will no doubt one day be able to understand and implement it all……

  • 10. anze  |  February 24th, 2006 at 6:02 pm

    Nice article.

    There is a simpler and easier approach to securing your forms though. Instead of checking input before calling mail(), tyou can instead call mail2(), which checks for proper input:

    Why? If you later discover that the script is still being abused you can easily change it.

    By the way, you might have noticed that I changed the two functions too - using negative function names is not intuitive and should be avoided. For instance, compare this:
    if (!has_no_newlines(…))
    with this:
    if (has_newlines(…))
    The meaning is the same, but the second form is much easier to understand.

    Happy coding!

    Anze

  • 11. anze  |  February 24th, 2006 at 6:04 pm

    Hey, how about leaving PHP tags where they were? :(

    Let's try again:

    <?php
    $to = 'contact@domain.com';
    $subject = $_POST["subject"];
    $message = $_POST["message"];
    $headers = "From: ".$_POST["from"];

    function mail2($to,$subject,$message,$headers)
    {
    // check $subject and $headers:
    if (has_newlines($subject))
    return(false);
    if (has_email_headers($headers))
    return(false);
    // send mail:
    mail($to,$subject,$message,$headers);
    };

    mail2($to,$subject,$message,$headers);
    ?>

    Anze

  • 12. matthijs  |  February 25th, 2006 at 5:58 pm

    Another and in my opinion a bit easier to use function is php's ctype_print() function. It will return true for printable characters and therefore false for newlines and carriage returns. I first read about it here and I immediately knew it would be usefull. The function is also mentioned by Chris in a recent article in php architect as a defense in depth measure for these type of problems.

  • 13. Rob  |  February 26th, 2006 at 9:10 pm

    You're better off using ctype_print() than a has_no_newlines regex, if for no other reason than that it's less code to worry about.

    I usually just run anything that's going to be in the headers through ctype_print(), run striptags() on the message body, and validate the email address with a regex.

    Another option that's a good bit safer is to put the user's "From" address in the body of the message, and use a hard-coded "From" email. That leaves zero access to the email headers and, as such, zero possibility of having your form hijacked.

  • 14. Chris H  |  February 28th, 2006 at 9:39 am

    So just to clear up the confusion waftign round my brain, if you put email addresses in a certain format in the From textarea then they can get processed and the form sends it to those addresses as well??

  • 15. Sarah  |  February 28th, 2006 at 11:33 am

    Chris yes you're right. If you view my post on secure forms you'll see an example that I actually had through about 2-3 weeks ago. My forms are hardcoded however you can see what the spammer tried to do in the "mobile" field. That should give a good example of what they can do.

    It's hard to visualise a spam attack until you see it like this, as it's the first one I've actually seen.

    Khalid, as ever, nice post :)

  • 16. Chris H  |  February 28th, 2006 at 11:40 am

    Thanks Sarah, I did read your post when it was posted but I have to be in the right frame of mind to look at PHP ;)

    And blimey, why is this 'functionality' in the php mailer anyways??

  • 17. Plans to Prosper » &hellip  |  February 28th, 2006 at 11:51 am

    […] Khalid and Sarah put it much more wisely though. […]

  • 18. Rhyll > PHP Blog >&hellip  |  March 1st, 2006 at 5:16 pm

    […] If you aren't careful, this simple little contact form can turn into mechanism for sending out spam. There is an excellent write up on email header injection on a contact form. The key point is allowing the end user to input their own from address. If that from address isn't properly validated the spammers can use it to inject additional headers, like bcc, which allows them to crank out the spam. […]

  • 19. Anze  |  March 3rd, 2006 at 3:24 pm

    matthijs, Rob:
    ctype_print won't disallow %0A (though I don't know if %0A really can be misused).

    Anze

  • 20. Anze  |  March 7th, 2006 at 12:52 pm

    And while we are at it - the best way of course would be if PHP developers would do something about this, like changing the parameters to we could use an array of one-line strings instead of strings.
    For instance:
    array("From: me@domain.com","Reply-To: reply@domain.com")
    instead of:
    "From: me@domain.comrnReply-To: reply@domain.com"

    Then mail() itself could check the headers and dispose of anything that would look like e-mail header injection. So one could use:
    mail("x@y.com","test 123",$message,array("From: ".$_REQUEST['from']));

    Happy coding!

  • 21. Khalid  |  March 7th, 2006 at 1:07 pm

    The issue here is how much hand-holding needs to be done? The PHP developers should be concentrating on other things than protecting the world from coders who are foolish enough to not validate their input.

  • 22. Matt  |  March 9th, 2006 at 5:58 pm

    I guess i just don't get why you NEED to have the "from" field have the sender's email. I mean it seems like hard coding a "to", "from", and "subject" then putting the user's email address and subject in your body would completely get around this problem right? To reply the recipient would just have to copy and paste the email address.
    -[ Trendy Box ]-

  • 23. IBS Helpdesk » Blog&hellip  |  March 14th, 2006 at 10:34 am

    […] http://www.jellyandcustard.com/2006/02/24/email-header-injection-in-php/ […]

  • 24. Anze  |  March 17th, 2006 at 10:16 am

    Khalid: that is exactly what they are doing now. Every system must try to make security easy to do, otherwise people tend not to do it. It would actually be pretty easy for PHP developers to fix this and they would make life easier for many PHP users. As you can see from the above post, validating input in this case is not that easy. Not all PHP coders are geniuses and everything that makes their life easier should be good news.

    Matt: I just recently switched from validating input to embedding "From" in the message - the next day I got a call from a customer why he can't hit a reply. Some forms are heavily used and users don't want to copy/paste every time.

    Another idea: you can validate input and if validation fails, you embed the fields in the message, otherwise you use them as headers.
    But I would use a more rigorous way of validating input.

    Regards,

    Anze

  • 25. Matt  |  March 17th, 2006 at 3:48 pm

    Well okay I can see that in some cases it would be an issue. The only thing I can say is that it IS more secure in the body.

    How about making it a link in the body like sending it as a mailto:AD@DRESS.com ? then assuming they viewed the email as html they could just click the address.

    I know that assumes a lot. and I appreciate what you said about your client and the point you made was good.
    -[TrendyBox]-

  • 26. Rick  |  March 20th, 2006 at 11:39 am

    This is all very good if you are familiar with PHP and how to use these headers but I have just started using a program that creates the form and validations for me.

    Problem is that my form is being hijacked by someone and I need to redo the headers I guess they are using better code I am told. BUT I have no idea how to merge them into what I already have.

    Can anyone help with this please?

  • 27. John  |  April 7th, 2006 at 7:21 pm

    This was a helpful article. Though after reading the different posts I think I will just hardcode the dangerous fields of the form.

    But, I do have a question. Even if this will stop spammers from using my form, will I still get blacklisted by any scans that major organizations are running over the internet if they detect my IIS web server as "open relay"? I have it setup to grant relay permissions to its loopback address. Will they catch that? Are there other dangers involved with that?

  • 28. dkappe  |  May 17th, 2006 at 2:55 pm

    Have you considered mod_security? It's like a stateful firewall for apache. Can catch email and sql injection attacks. Open source, of course.

  • 29. BPE Development Blog &raq&hellip  |  June 20th, 2006 at 8:51 pm

    […] After reading many articles on email header injection and how to avoid it, I found a simple solution. The htmlMimeMail class, which has a nice API and is used on many open source projects including Gallery, can be subclassed to escape newlines in header values. The technique is the same as that used by the Zend Mail class, but Zend Mail doesn't work in PHP4, and I like the htmlMimeMail API better anyway. So, here's the quick fix: […]

  • 30. Jonathan Mueller  |  August 26th, 2006 at 1:12 am

    am i missing something here? i've been trying to solve for the problem of email injection attacks all day and i can't get my code to capture anything matching the traps that have been set.

    $form_data = array
    (
    "first_name",
    "last_name",
    "email",
    "subject",
    "comments"
    );

    //JMM: Loop through form data

    foreach ($form_data as $value)
    {
    //JMM: Grab data
    $$value = stripslashes($_POST[$value]);
    //JMM: Decode data
    $$value = urldecode($$value);
    //JMM: Strip tags to remove potential risky HTML code
    $$value = strip_tags($$value);
    //JMM: Match against specific injection risks and redirect back to form if encountered
    if (preg_match("/(%0A|%0D|n+|r+)(content-type:|to:|cc:|bcc:)/i", $comments))
    {
    header("Location: http://$host$uri/$page");
    exit();
    }
    }

  • 31. Jonathan Mueller  |  August 26th, 2006 at 1:20 am

    i just realized in the above that the $comments variable in the preg_match line was a leftover from my test. it was originally set to $$value

  • 32. Jonathan Mueller  |  August 26th, 2006 at 1:33 am

    and maybe i'm not just understanding something fundamental here. i'm testing the form by typing the characters "n" in an input field and seeing if it will redirect after I click submit. it never ridrects so i don't think the code is capturing the "n" input

  • 33. Khalid  |  August 26th, 2006 at 9:17 am

    the n for new line is added automatically by the browser when you hit in a textarea. This won't happen in an input field. However, if you only check the textareas on your form, it means that someone could create their own form, use textareas on all fields, and point it to your form's action - thus exploiting your code.

  • 34. miri  |  September 12th, 2006 at 1:39 am

    * Returns false if text contains newline followed by
    * email-header specific string

    Am I on crack or do the functions actually return true if they find email headers and false if they don't?

  • 35. sarahG  |  November 3rd, 2006 at 7:58 pm

    miri - the functions return true if the email address is okay.

  • 36. sarahG  |  November 3rd, 2006 at 8:18 pm

    Sorry I was wrong (just using it on a site myself!). The functions has_no_newlines and has_no_emailheaders return false if the emails are fine. I'm not too hot on expressions but I think these functions are finding a match for a newline or email header, so if one isn't fine then the function will come back false as nothing (bad) is found.

    So in my check I have it saying that if either function comes back true then there is an error.

  • 37. Trevor L.  |  December 8th, 2006 at 6:01 am

    Thanks so much for addressing this issue. One of my clients' forms has been getting hit today, so I set up a script to capture the url they were posting from, the IP, all the POST data, etc. It was interesting to see what they were trying.

    Oddly enough they were somehow forging the from the domain itself (and therefore getting past my url authentication). I found something that might be helpful at http://phpsec.org/projects/guide/2.html that will at least guarantee that the submission is coming from your form.

    Stripped down, it creates a random 'token' and both prints it into a hidden field AND stores it as a session variable. The submitted token has to match the current session in order to run the mail code. Here's an example with the form as an include file:

    This goes in the form:

    " />

  • 38. Trevor L.  |  December 8th, 2006 at 6:05 am

    Oops. Here it is without the tags.

    PHP code:

    session_start();

    if (isset($_SESSION['token']) && $_POST['token'] == $_SESSION['token']) {
    // code in this section is protected by the token
    mail($var1,$var2,$var3,$var4);
    } else {
    $token = md5(uniqid(rand(), true));
    $_SESSION['token'] = $token;
    include('my_form.php');
    }

    Make sure the form has a hidden field with:

    name="token" value="PRINT THE TOKEN VAR HERE"

  • 39. kool benny  |  December 18th, 2006 at 12:10 pm

    I echo Tony's queries above… I am not a PHP programmer, I have a few contact forms using the mosts basic of sendmail scripts. Now one is getting abused and so am looking to fix it!

    Would this be a correct way to implement this validation?

    My other problem is that I don't really know how to test it. Do I need to pretend to send spam using my own form for testing purposes?

    Many thanks

    $value) {
    $_POST[$key] = (!has_newlines($value)) ? '' : $value;
    }

    foreach($_POST as $key=>$value) {
    $_POST[$key] = (!has_emailheaders($value)) ? '' : $value;
    }

    if (eregi("www.domain.com", $_SERVER['HTTP_REFERER'])) {
    mail($to, $subject, $message, $headers);
    header("location: thankyou.html");
    }
    else {
    echo "Forbidden";
    exit;
    }
    ?>

  • 40. kool benny  |  December 18th, 2006 at 12:12 pm

    whoops – here's the code


    $to = "name@domain.com";
    $subject = "test form";
    $message = $_POST['message'];
    $message = wordwrap($message, 70);
    $headers = "From: " . $_POST['email'];

    /**
    * Check single-line inputs:
    * Returns false if text contains newline character
    */
    function has_newlines($text) {
    return preg_match("/(%0A|%0D|\n+|\r+)/i", $text) == 0;
    }

    /**
    * Check multi-line inputs:
    * Returns false if text contains newline followed by email-header specific string
    */
    function has_emailheaders($text) {
    return preg_match("/(%0A|%0D|\n+|\r+)(content-type:|to:|cc:|bcc:)/i", $text) == 0;
    }

    foreach($_POST as $key=>$value) {
    $_POST[$key] = (!has_newlines($value)) ? '' : $value;
    }

    foreach($_POST as $key=>$value) {
    $_POST[$key] = (!has_emailheaders($value)) ? '' : $value;
    }

    if (eregi("www.domain.com", $_SERVER['HTTP_REFERER'])) {
    mail($to, $subject, $message, $headers);
    header("location: thankyou.html");
    }
    else {
    echo "Forbidden";
    exit;
    }

  • 41. Carpenter  |  January 26th, 2007 at 7:38 pm

    I think captcha or a math problem as along with $strip_html_tags handles it pretty well for me.

  • 42. koolbenny  |  February 21st, 2007 at 4:33 pm

    Thanks everybody, here's my final working script:
    http://benclarke.blogspot.com/2007/02/php-email-header-injection-prevention.html

  • 43. schrose  |  April 10th, 2007 at 1:38 am

    Try this solution.
    http://beauford.vox.com/library/post/beaus-php-5-mail-logging-solution-for-windows-server-2003.html

  • 44. Bernie Zimmermann  |  May 17th, 2007 at 6:36 am

    Thanks for the great article. I just found out today that spammers have been abusing my contact forms, so this was a great resource.

    I tweaked some of the functions a bit so they're a bit more logical, and fixed the regular expression for email validation since the one provided doesn't escape the "dots" correctly.

    I've posted a summary of the changes over at my blog.

    Thanks again!

  • 45. Brain Goo » Blog Ar&hellip  |  May 21st, 2007 at 5:43 pm

    […] http://www.jellyandcustard.com/2006/02/24/email-header-injection-in-php/ […]

  • 46. Sys Admin Nexus » B&hellip  |  June 4th, 2007 at 1:12 pm

    […] Taken from jellyandcustard.com […]

  • 47. Graham  |  April 4th, 2008 at 6:41 am

    I have also found it useful to collect the IP address of people who respond using my forms and creating a list of IP addresses that attempt to break the sanitisation, and then:


    $IP_addr = $_SERVER['REMOTE_ADDR'];
    $banlist = fopen("badIP.txt", "r");
    $badips = "";
    while ( $data = fgets($banlist) ) {
    $badips .= $data . ";";
    }
    if ( strstr($badips, $IP_addr) ) die();

  • 48. George K.  |  July 14th, 2008 at 10:29 am

    I'm sorry. It's getting dark overhere and I mis-spelled my own name in the email address.
    Once again.
    First of all. No-one ever mentions the version of PHP as used. As a beginner on php I understand that there is a lot of difference between PHP4 and PJP5.
    Second.
    Why - in the example of this article a textarea is used for the From address. Wouldn't it be more safe to use a textfield with a lebgth of e.g. 30 characters?
    GHK

  • 49. Create a Contact Page Par&hellip  |  August 24th, 2008 at 3:02 pm

    […] Once you've got an established blog you'll most likely want a contact page with a contact form on it. Whilst there are a few plugins that do this for you, I tend to find that they either bloat your pages with additional CSS code in the header (CSS code should always be put in an external stylesheet whenever possible), badly written form markup, or probably the worst culprit, the PHP code doesn't validate the information in the form to help prevent spam or email header injection. […]

  • 50. Create a Contact Page Par&hellip  |  August 31st, 2008 at 3:00 pm

    […] There are 3 functions we can use to validate the email address. These functions were originally written by Khalid Hanif and there is also more info on his blog post concerning these. The functions are […]

Leave a Comment

Required

Required, hidden

Some HTML allowed:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>

Trackback this post  |  Subscribe to the comments via RSS Feed


Calendar

February 2006
M T W T F S S
« Jan   Mar »
 12345
6789101112
13141516171819
20212223242526
2728  

Most Recent Posts