Email Injection

Published in PHP Architect on 25 Jan 2006

I must admit that when I first heard about email injection a few years ago, I wasn’t very impressed. After all, it’s just another case of developers making the mistake of blindly trusting user input. If you let users manipulate the arguments passed to the mail() function, they can send email from your server. No big surprise there.

There are an alarming number of email injection vulnerabilities in PHP apps, and this has prompted me to focus on email injection in this month’s column. The popularity of this type of vulnerability has become a beloved treasure trove for spammers around the world, but why is it so common?

I think the root cause of email injection’s popularity is that developers don’t understand the attack or the necessity of filtering input prior to passing it to the mail() function. I think there are incorrect assumptions being made about how PHP sends email.

Sending Email

Most veteran PHP developers know all about the mail() function, and they realize that it provides a rather raw interface for sending email. Like many PHP functions, it is very flexible — mail() provides enough functionality to send almost any type of email you can imagine, provided you know the proper format.

As with most security vulnerabilities, it isn’t the experts who are making the mistakes. Using mail() is very simple, and the possibilities aren’t immediately obvious to a novice developer. For example, the following demonstrates a basic use:

  1. mail('to@example.org', 'My Subject', 'My Message');

A simple email injection vulnerability is to let a user provide the first argument:

  1. mail($_POST['email'], 'My Subject', 'My Message');

This is similar to any other injection vulnerability, but the context is different. If you try this yourself and provide your own email address, you’ll receive an email similar to the following (some headers removed for clarity):

  1. To: you@example.org
  2. Subject: My Subject
  3. From: nobody@localhost
  4.  
  5. My Message

The value of the To header is provided by the user, so someone wanting to exploit this situation might try to send spam from your server by simply providing a list of addresses:

  1. chris@example.org, rasmus@example.org, andi@example.org

You can mimic this situation with a simple test:

  1. mail('chris@example.org,
  2.   rasmus@example.org,
  3.   andi@example.org',
  4.   'My Subject',
  5.   'My Message');

Each of these addresses will receive the email, because the header can handle multiple addresses in this format.

Many developers mistakenly assume that this problem represents the extent of the concern, so it’s not a concern if the recipient is static. This isn’t true. A larger problem is when the fourth argument to the mail() function is provided in part by the user.

Injecting Headers

A common use of the mail() function is to provide a contact or feedback form. An example of such a form is as follows:

  1. <form action="sendmail.php" method="POST">
  2. <p>Your Email:<br />
  3. <input type="text" name="email" /></p>
  4. <p>Your Subject:<br />
  5. <input type="text" name="subject" /></p>
  6. <p>Your Message:<br />
  7. <textarea name="message"></textarea></p>
  8. <p><input type="submit" /></p>
  9. </form>

The trouble with using the mail() function, as demonstrated earlier, is that the email appears to be sent from your web server — the From header is generated by PHP automatically. If you want to let customers send you email through a page on your website, you need to be able to set this, so that the email appears to be from them. This is where using the mail() function’s fourth argument helps:

  1. mail('to@example.org',
  2.   'My Subject',
  3.   'My Message',
  4.   'From: from@example.org');

By letting you specify additional headers, PHP gives you the flexibility you need to specify the From header. This is great, but it is also where the real danger of email injection lurks. Consider the following example for the sendmail.php script referenced in the previous contact form:

  1. mail('contact@example.org',
  2.   $_POST['subject'],
  3.   $_POST['message'],
  4.   "From: {$_POST['email']}");

If you test this yourself, you’ll see that it works as expected. You receive an email at your contact address just as if the user had emailed you directly. Unfortunately, users now have almost absolute control over the email. The most common tactic used by spammers is to provide the spam message in the contact form and attempt to provide additional headers in the email field. For example, they can provide an email such as the following to send the message to an additional recipient:

  1. foo@example.org
  2. To: victim@example.org

The trouble with this approach, from a spammer’s perspective, is that you’re more likely to notice the vulnerability. The contact@example.org address will receive an email similar to the following:

  1. To: you@example.org
  2. Subject: My Subject
  3. From: from@example.org
  4. To: victim@example.org
  5.  
  6. My Message

Although the email is sent to both you@example.org and victim@example.org as desired, the exploit is quite obvious. Spammers don’t succeed by exploiting a script once — they want to find a vulnerable script and exploit it for a long time. This makes the Bcc header a favorite injection:

  1. from@example.org
  2. Bcc: victim@example.org

As many Bcc headers as desired can be provided, and the resulting email will be much less conspicuous:

  1. To: you@example.org
  2. Subject: My Subject
  3. From: from@example.org
  4.  
  5. My Message

Because the Bcc header is not present in the message, you’re more likely to think that a spammer has simply tried to spam you personally using your online form. After all, you’re allowing anyone to send you a message, and this doesn’t appear to break the rules in any way. However, an unknown number of other people might have received the same spam message, and your script’s URL will become a favorite spammer destination.

Exploiting Vulnerable Scripts

In order to test your own scripts, you want to be able to provide more than one line for the email, as demonstrated in a previous example:

  1. From@example.org
  2. Bcc: victim@example.org

There are a few ways to accomplish this. The simplest is to type it out in a text editor, copy it, and paste it into the form. Of course, spammers opt for something a bit more automatic. The following PHP script exploits a contact form hypothetically located at http://example.org/sendmail.php:

  1. <?php
  2.  
  3. $fp = fsockopen('example.org', 80);
  4.  
  5. fputs($fp, "POST /sendmail.php HTTP/1.1\r\n");
  6. fputs($fp, "Host: example.org\r\n");
  7. fputs($fp, "Content-Type: application/" .
  8.   "x-www-form-urlencoded\r\n");
  9. fputs($fp, "Content-Length: 95\r\n\r\n");
  10. fputs($fp, 'email=from%40example.org' .
  11.   '%0D%0ABcc%3A+victim%40example.org' .
  12.   '&subject=My+Subject&message=My+Message');
  13.  
  14. fclose($fp);
  15.  
  16. ?>

The format of POST data is exactly the same as the format of GET data, and the URL-encoded CRLFs appear as %0D%0A.

Although I do not wish to detract from the focus of the article, keep in mind that more sophisticated attacks can be used to send HTML email, attachments, and the like. An attacker, given complete control over the arguments to the mail() function, can do anything PHP is capable of.

Preventing Email Injection

As I hope is already clear, preventing email injection is a simple matter of filtering input. In this case, filtering with a whitelist approach isn’t easy. You can probably restrict the subject to a whitelist of valid characters, but you might need to be more lenient in the message. Email addresses have proven difficult to filter for a number of reasons, including the fact that the specification isn’t very restrictive. (Did you know an email address can have comments in it?)

My advice is to do the best you can and consider some defense in depth approaches to strengthen your filtering. There are numerous regular expressions that can help you filter an email address, and even the more lenient examples prevent common email injection attacks:

  1. <?php
  2.  
  3. $clean = array();
  4.  
  5. $email_pattern =
  6.   '/^[^@\s<&>]+@([-a-z0-9]+\.)+[a-z]{2,}$/i';
  7.  
  8. if (preg_match($email_pattern, $_POST['email'])) {
  9.   $clean['email'] = $_POST['email'];
  10. }
  11.  
  12. ?>

A good defense in depth approach is to inspect the data specifically for newlines and carriage returns, and the ctype_print() function can help:

  1. if (ctype_print($clean['email'])) {
  2.   /* The email contains no newlines or carriage returns. */
  3. }

This technique can potentially save the day in the event that your filtering logic has a flaw.

Until Next Time…

I hope this article helps you appreciate the need to consider security in every aspect of your PHP development, even those simple contact and feedback forms. By inspecting input to be sure that it’s the format and size that you expect, you can prevent many types of vulnerabilities, email injection included.

Defense in depth measures such as checking for carriage returns and newlines are very useful, but try to resist the urge to rely on these techniques as primary safeguards.

Until next month, be safe.