Forum Moderators: coopster

Message Too Old, No Replies

contact form - preventing header injection

         

mihomes

6:36 am on Jan 3, 2007 (gmt 0)

10+ Year Member



Ok, now this is going to sound really dumb, but please be aware I have been sleepless for some time now haha.

I am in the process of creating a php contact form and want to prevent header injection (cc, bcc, etc) so as to prevent spam from my script to others.

After looking at my code I realized... the only value I need to validate is the email field the user inputs. Correct me if I am wrong though.

$headers = "From: " . $_POST['Email'] . "\n" . "Return-Path: " . $_POST['Email'] . "\n" . "Reply-To: " . $_POST['Email'];
mail('support@mysite.com','mysite Support',$message,$headers);

So, my theory is that since $_POST['Email'] is the only value the user inputs on my form which IS part of the header this is the only input I need to check - correct? If I validate that with the below :

if(isset($_POST['Email']) &&!empty($_POST['Email'])){
$_POST['Email'] = stripslashes($value);
if(!preg_match("/^[a-z0-9_.-]+@[a-z0-9_.-]+.[a-z]{2,6}$/i",$_POST['Email'])){
$errors[] = "Email address is invalid";
}
}

... I should be good to go. I realize that is not stopping a spammer from sending me advertisements in my comment field, or fake names in the name field, but if they do I am the ONLY person who will receive the spam - in other words there is no way for them to inject cc and bcc emails and send out to multiple people - correct?

Man, I need some sleep...

hughie

10:13 am on Jan 3, 2007 (gmt 0)

10+ Year Member



Alas, if that were only true.

Just about any field can be used to inject data if the person forms the email correctly.

My standard practice is to:
- put character limits on name, email and any other basic inputs, A much longer chartacter limit on the message.

- Validate the email address field, making sure of just one @ character

- Strip all @ characters in all other fields, replacing them with " AT "

- Make sure the "send to" email address is not being defined by the form.

- you could also make sure the referrer of the page is from your server, however this might be possible to spoof, not 100% here.

- If any of this is going into a database you need to do further validation to make it safe for insertion.

This approach seems to have cut out possible email injections across my sites whilst not cutting out anything i actually wanted.

Cheers,
hughie

barns101

2:12 pm on Jan 3, 2007 (gmt 0)

10+ Year Member



I also check all fields for "MIME-Version:", "Content-Type" and "@mydomain.com" because these are often used in spamming attempts (both header injection and advertisements).

TeddyBeer

2:29 pm on Jan 3, 2007 (gmt 0)

10+ Year Member



Be sure to end headers with double return and line feeds. Like so:

<?php
$headers =
'X-Mailer: PHP/' . phpversion() . "\r\n" .
"MIME-Version: 1.0\r\n" .
"Content-Type: text/html; charset=utf-8\r\n" .
"Content-Transfer-Encoding: 8bit\r\n" .
"Message-ID: <".date ('D, j. M Y, H:i')."info@".$_SERVER['SERVER_NAME'].">\r\n" .
"From: $from_name <$from_email>\r\n" .
"Reply-To: $from_name <$from_email>\r\n" .
"Subject: $subject\r\n\r\n";
?>

rjwmnews

3:56 pm on Jan 3, 2007 (gmt 0)



The following URLs cover the topic in detail.

[webmasterworld.com...]

[securephpwiki.com...]

It's a good idea to check for any line feed and carriage return characters which are used to separate mail header lines.

<?php
$from = urldecode($_POST['sender']);
$pattern = '/[\r\n]/i';

if ( preg_match($pattern, $from) ) {
// Do something
// -- or --
// die('Error message...');
}
?>

Refer to the PHP function documention for urldecode() to review the user-contributed UDF (user defined function) 'unicode_urldecode($url)' to handle unicode characters.

rjwmnews

4:05 pm on Jan 3, 2007 (gmt 0)



Likely useful code sourced from an unknown author (untested).

<?php

$header_injection_attempts = array(
'bcc:',
'cc:',
'to:',
'content-type:',
'mime-version:',
'multipart/mixed',
'content-transfer-encoding:'
);

$email_body_lower = strtolower($email_body);
$injection_attempted = false;

foreach($header_injection_attempts as $attempt){
if(strpos($email_body_lower, $attempt)!==false){
$injection_attempted = true;
break;
}
}

if($injection_attempted) {
// log the error and remote IP
// don't send the email
} else {
// send the email
}

?>

barns101

4:50 pm on Jan 3, 2007 (gmt 0)

10+ Year Member



That code looks great, but would probably be better using case-insensitive stripos() [uk.php.net].

** Edit
Just noticed that it converts the string to lowercase before checking.

mihomes

7:55 pm on Jan 3, 2007 (gmt 0)

10+ Year Member



Thanks for the replies guys. I guess I am still confused. On my form I take the following fields - Name, Email, OrderID, Questions - then pass them onto a php page.

If I test Email with :

if(isset($_POST['Email']) &&!empty($_POST['Email'])){
$_POST['Email'] = stripslashes($value);
if(!preg_match("/^[a-z0-9_.-]+@[a-z0-9_.-]+.[a-z]{2,6}$/i",$_POST['Email'])){
$errors[] = "Email address is invalid";
}
}

That ensures I only have one email listed in the Email form and also that its in the form above. That test right there will prevent any new lines, bcc, cc, content type, whatever have you from being inserted in that field.

Now, if you look at my header :

$headers = "From: " . $_POST['Email'] . "\n" . "Return-Path: " . $_POST['Email'] . "\n" . "Reply-To: " . $_POST['Email'];

Email is the only field inserted into the headers out of my original inputs AND it is already valid.

I guess my question is, since Name, OrderID, and Questions are simply inserted into $message then sent with :

mail('support@mysite.com','mysite Support',$message,$headers);

wouldn't the rfc specs for mail() disable anything from the other fields? It sure does look like that way to me, but then again it appears as people are stating it IS possible.

EDIT : Be aware I am not concerned with form spam to myself (support@mysite.com) I am only concerned with them injecting the form to send spam to other addresses than me.

hughie

9:06 pm on Jan 3, 2007 (gmt 0)

10+ Year Member



Just because you're setting the headers, doesn't mean the spammer can't set his own headers as part of the $message field, that's the whole problem.

A maliciously formed request will disregard your headers and insert his own through the $message variable, thus allowing said spammer to send emails to as many people as they like via your server, not just to you.

whoisgregg

10:07 pm on Jan 3, 2007 (gmt 0)

WebmasterWorld Senior Member 10+ Year Member



Likely useful code sourced from an unknown author (untested).

Hi! :)

Here's a link to a good library thread we had on combatting webform hijacking [webmasterworld.com] last summer (which includes that code snippet and some links to more resources).

mihomes

2:02 am on Jan 4, 2007 (gmt 0)

10+ Year Member



okay, after some testing here is what I came up with... opinions and suggestions welcome :

<?php

// Specify which fields to require from form
$required_fields = array('Name','Email','Questions');

// Initialise variables

$errors = array();
$message = "";

// Remove whitespace, tab, new line, carriage return, nul byte, and vertical tab from beginning and end of all inputs
// Check all inputs for injection

foreach($_POST as $key => $value){
$_POST[$key] = trim($value);
$_POST[$key] = cleaninjections($value);
}

function cleaninjections($test)
{
// Remove injected headers
$find = array("/bcc\:/i",
"/Content\-Type\:/i",
"/Mime\-Version\:/i",
"/cc\:/i",
"/from\:/i",
"/to\:/i",
"/Content\-Transfer\-Encoding\:/i");
$ret = preg_replace($find, "", $test);
return $ret;
}

// Check required fields and return error if blank

foreach($required_fields as $value){
if(!isset($_POST[$value]) ¦¦ empty($_POST[$value])){
$errors[] = "Please go back and complete the $value field";
}
}

// Validate name field. Accepts a-z, space, period for Dr., and ' for O'Malley

if(isset($_POST['Name']) &&!empty($_POST['Name'])){
if(!preg_match("/^[a-z '.]+$/i",stripslashes($_POST['Name']))){
$errors[] = "You have submitted an invalid character in the Name field";
}
}

// Validate email field. Accept limit is joe1_mike.sch-moe@te_st.te-st.museum

if(isset($_POST['Email']) &&!empty($_POST['Email'])){
if(!preg_match("/^[a-z0-9_.-]+@[a-z0-9.-]+.[a-z]{2,6}$/i",stripslashes($_POST['Email']))){
$errors[] = "Email address is invalid";
}
}

// Validate OrderID field. Only allows letters, numbers, and hyphen

if(isset($_POST['OrderID']) &&!empty($_POST['OrderID'])){
if(!preg_match("/^[a-z0-9-]+$/i",$_POST['OrderID'])){
$errors[] = "You have submitted an invalid character in the Order ID field";
}
}

// Display any errors and exit if errors exist.

if(count($errors)){
foreach($errors as $value){
print "$value<br>";
}
exit;
}

// Create the email
$message .= "*******************************************************************************\n";
$message .= "Name: " .$_POST['Name']."\n";
$message .= "Email: " .$_POST['Email']."\n";
$message .= "Order ID: " .$_POST['OrderID']."\n";
$message .= "IP address: " .$_SERVER[REMOTE_ADDR]."\n";
$timestamp = time();
$message .= "Date: " .date("D, F d, Y",$timestamp)."\n";
$message .= "Time: ". date("h:i:s A",$timestamp)."\n\n\n";
$message .= "Questions:\n\n" .$_POST['Questions'];

// remove all '\' as extra precaution against \r and \n
// strips all html and php tags

$message = str_replace("\\", "", $message);
$message = strip_tags($message);

// Sends email

$headers = "From: " . $_POST['Email'] . "\r\n" . "Return-Path: " . $_POST['Email'] . "\r\n" . "Reply-To: " . $_POST['Email'];
mail('support@mysite.com','MySite Support',$message,$headers);

// Redirect location

header("location: [mysite.com...]
exit;

?>

Now, eventhough it has been said here I am finding conflicting reports that one cannot inject (not spam... INJECT) forms which are not part of the header. If someone can definately clear this up I would appreciate it. If that is true I ONLY need to validate my email as that is the onlyinput put into the headers. Now, that won't mean someone will spam me with an advertisement in the Questions section, but they cant cc, bcc, or send out the same email to someone other than me. If someone can provide absolute proof of this I would appreciate it. I am reading the mail() RFC will not allow other parts of the email ($message) to affect the headers.