Forum Moderators: coopster
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...
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
<?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";
?>
[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.
<?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
}
?>
** Edit
Just noticed that it converts the string to lowercase before checking.
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.
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.
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).
<?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.