Building a Bulletproof Contact Form with PHP : Comments
October 15, 2007
Comments
Would it be god to test if the sending file (process-form.php) is called from the form.html file to avoid that the process-form.php script is directly called by a boot?
How could it be done?
Yes, that would indeed be another additional security measure that you might want to put in place.
You could do it by adding a single line at the top of the process-form.php script:
if (stristr($_SERVER[‘HTTP_REFERER’], ‘form.html’) === FALSE) die(‘No direct script access permitted’);
Thank you for the quick and helpfull reply.
My PHP knowledge looks like Swiss chese, and the article filled in nicely.
As referrers cannot be trusted, one could set a unique token in the form and session every time it’s loaded and check for that within the script processing the form.
However this will still be fooled by scripted session capable UAs.
There’s another trick that may be worth exploring to prevent automated scripted spam. The technique was mentioned by Steve Champion at SXSWi 07.
All you need is an extra form element that’s hidden from view if the form is submitted with anything in it you know it’s automated spam. This works on the basis that automated spam will submit something in every field. There’s clearly some accessibility issues to worry about here, but it may be worth a look.
Way to go the extra mile and talk about header stripping, database issues and the like, Matthew (not sure what that guy above was going on about).
WRT Spam, as Stuart mentioned above, adding a little honey pot works. I’ve also added a hidden field to forms that holds a salted md5 hash that changes on a daily basis. If that field is missing, the form doesn’t go thru. This can help to throw off some types of form spamming issues.
To Mike Papagerge: It’s a good idea add md5 hash to forms. I want to do it on my site too.
Grr.. Just to correct my comment above, if the md5 hash is incorrect or missing, the form doesn’t go thru. This can help to throw off some types of form spamming issues.
Great article and nice additional tips here in the comments!
One question I had though – I am unsure of exactly what the preg_replace part was doing. Can someone clarify that or elaborate for me?
Hi Mike – the PHP preg_replace function is a regular expression string replacement function. It basically says “Replace any occurrences of X with Y in string Z.”
You can also pass arrays into the ‘find’ and ‘replace’ parts, which is what is happening in the article – we are telling the function to look for each element in the array, and replace each one with a blank string (i.e. remove them completely).
Matthew, well done. As I started reading the article I saw it transition from mail() to database stuff and I thought “MATTHEW! What about sanitizing? Are you insane?”… but then, as if on cue, you came in and cleaned that up. Very clear and well written Matt. I’ll be using this as a resource for my students, and when I need to explain contact forms.
Thanks for taking the time.
Well done on the article. This is an important subject as PHP forms too often seem to be the exploit of choice for spammers. With a little extra care 99% of spam can be avoided. @Mike P: fine suggestion on the hidden MD5 hash, I’ll have to remember that one! :)
I was excited to finally find a simple, safe form processor. Thank you. My problem is that I don’t know PHP and have no interest in learning it if I can help it. Can you tell me if the following is an accurate summary of what you recommend (without using a database)?
<?php
// Process only from my form (change if html has different name)
if (stristr($_SERVER[‘HTTP_REFERER’], ‘form.html’) === FALSE) die(‘No direct script access permitted’);
// Data cleaning function
function clean_data($string) {
if (get_magic_quotes_gpc()) {
$string = stripslashes($string);
}
$string = strip_tags($string);
return mysql_real_escape_string($string);
}
// Mail header removal
function remove_headers($string) {
$headers = array(
“/to\:/i”,
“/from\:/i”,
“/bcc\:/i”,
“/cc\:/i”,
“/Content\-Transfer\-Encoding\:/i”,
“/Content\-Type\:/i”,
“/Mime\-Version\:/i”
);
$string = preg_replace($headers, ‘’, $string);
return strip_tags($string);
}
// Pick up the cleaned form data
$name = remove_headers($_POST[‘name’]);
$email = remove_headers($_POST[‘email’]);
$topic = remove_headers($_POST[‘topic’]);
$comments = remove_headers($_POST[‘comments’]);
// Build the email (replace the address in the $to section with your own)
$to = ‘contact@mysite.com’;
$subject = “New message: $topic”;
$message = “$name said: $comments”;
$headers = “From: $email”;
// Send the mail using PHPs mail() function
mail($to, $subject, $message, $headers);
// Redirect
header(“Location: success.html”);
?>
Hi Azriel – if you’re not using a database, you don’t need the clean_data() function (as it’s not being called). Apart from that, yes – that is all that you need to process your contact form.
Obviously if you change the fields on your own contact form (e.g. to add a telephone number) you would need to add to the PHP to pick up that additional data, but it’s simply a case of copy+pasting the variables and changing the name accordingly.
Hello,
I’ve put the exact script from Azriel on my Server but it doesn’t work :-( After filling out the form there is a parse error:
Parse error: parse error, unexpected T_STRING in /homepages/2/d108199407/htdocs/process-form.php on line 9
What does that mean?
Kind regards
.Carsten
Some remarks about the preg_replace part, just because I’m quite obsessed with clean and optimized regular expressions. Hope you appreciate it.
First of all, colons do not need to be escaped. The same goes for dashes if they are not inside a character class.
Also I would combine the array into one long alternated regular expression. That way you only need to call preg_replace just once. You could optimize further and combine Content-Transfer-Encoding: and Content-Type: by pulling the first Content-T part outside the alternation.
Then finally, why not use PHP’s strireplace function in this case? The power of preg_replace is great, but if you don’t really need it, go for the speed of strireplace.
Carsten: If you have copy+pasted the script above, make sure that you haven’t got ‘smart quotes’ (i.e. angled ones, which our comment system has used). All single- and double-quotes should be the standard ones you type with your keyboard.
Thank you!!! It works after fixing the smart quotes from the code that I copied from Matthew (above). I really appreciate your responsiveness. Now if I could only get you to fix my javascript woes I’m having elsewhere. :-)
I fixed the code formatting for preview.
You can use the [at] symbol or bc. to format code.
Why would you keep processing the form results if you detect any e-mail headers? Wouldn’t it be better to look for the headers with a regular expression search and kill the pocess if a header is detected. There is no need to store the results in a database or send the e-mail since it’s junk.
I’m curious, if “magic quotes” are turned on and your form data is already escaped…what’s the difference between the already escaped data and data that is escaped with the mysql_real_escape_string() function?
@Patrick – magic quotes and mysql_real_escape_string() handle multi-byte character encoding differently (more detailed explanation).
Thanks for the hints – I should have seen it by myself, because the colors of the code in my editor were the wrong ones :-)
Now nearly everything works – except the success.html doesn’t show…
Kind regards
.Carsten
You might also want to add a request-type check to the ‘processing’ page, so people can’t hit it directly.
if ($_SERVER[‘REQUEST_METHOD’] != ‘POST’) {
header(‘Location: form.html’);
}
Can’t quiet get it to work.
The script is definatly being activated, but I can’t tell if the mail is actually being sent. I’m not getting my test email.
How can I diagnosis that will let me know that that mail is indeed being sent from the server?
Thanks
@Rob:
If you wrap the mail() function call in an if…then block, you can catch any errors occurring:
if (mail($to, $subject, $message, $headers)) {
// Successful send
header("Location: success.html");
} else {
// Function call failed
die("There was a problem sending your email.");
}
Hi Matthew. I’d like to offer one that I made (also hardened). It’s available as a stand-alone and as a WordPress version:
SO: http://green-beast.com/blog/?page_id=71
WP: http://green-beast.com/blog/?page_id=136
Mine doesn’t interact with a database though so some of the controls are not present.
Hi Matthew,
Thanks for sharing your insights into a simple PHP contact form and taking the time out to walk us through it and giving some basic advice in preventing spam, much appreciated.
Hi & Thanks for a good Article.
I know it is probably a little off the scope of this article – being about usability rather than security, but I always think that it is nice to have default values in the fields when you create a form.
For example in this case of this comments form the default value for the Name field could be ‘Your Name Here’, then when you give that field the focus, the default value clears (using Javascript).
It’s not a big thing but I think it is a nice touch, and default values might also be a requirement to get AAA rating for web accessibility from Bobby / http://webxact.watchfire.com/ (although I’m not sure off the top of my head)
Thanks for this article Mark. It’s really helpful. Could I suggest if you ever do a follow up, maybe add some form validation as well?
I’ve been scouring the internet trying to find info on form validation and not having much luck so far!
Thanks again.
Very nice article, and one I’m adding to a site I’m developing as I’m writing this.
John Elliot said “default values might also be a requirement to get AAA rating for web accessibility from Bobby”.
To set the record straight- the requirement for default values in form fields is obsolete.
As is “Bobby”. The guidelines for accessibility are written by the w3c’s Web Accessibility Initiative (WAI) at http://www.w3.org/WAI/
Just a quick question, will this form be safe from header_injections?
Bjarni – yes, the section that deals with preg_replacing all the potential headers (To:, Cc:, etc.) deals with that risk.
Cheers Matthew, I will read up on it some more, much appreciated for your time and insights on this PHP contact form.
PHP Parse error: parse error, unexpected T_STRING in the line I have the
if (stristr($_SERVER[‘HTTP_REFERER’], ‘form.html’) === FALSE) die(‘No direct script access permitted’);
part
No idea why this fails
@Carlos: I would check that you’ve not got smart quotes (the curly version instead of the keyboard quote) in your code, as that can cause that error; it’s quite common when copying+pasting code from another source.
This has been a really useful article for me as a PHP newcomer.
I’m just struggling a little with the question of denying requests to the form processor from locations other than the form itself. Our form can appear to come from three different locations – the page itself, its ID within the CMS, and a marketing alias which is done via an Apache rewrite rule. So I need to allow all three to come through and deny everything else.
I’m guessing I need an if/then/else setup. But since the ‘then’ has to contain basically everything else in the processing script, does that introduce an overhead before the spam request is blocked? Does it expose anything in doing so?
Great script but I have not a big clue about PHP and struggeling to make it work ….
It is not sending and I have wrapped the mail function but nothing I could figure out.
I want to use a captcha at the beginning and maybe that causes the problem?
If you are interested that is how it looks like:
<?
session_start();
if ($_POST[“vercode”] != $_SESSION[“vercode”] OR $_SESSION[“vercode”]==’‘) {
echo ‘<strong>Incorrect verification code.</strong><br>’;
} else {
header(“Location: cthanksnewsletter.php”);
};
?>
<?php
// Mail header removal
function remove_headers($string) {
$headers = array(
“/to\:/i”,
“/from\:/i”,
“/bcc\:/i”,
“/cc\:/i”,
“/Content\-Transfer\-Encoding\:/i”,
“/Content\-Type\:/i”,
“/Mime\-Version\:/i”
);
$string = preg_replace($headers, ‘’, $string);
return strip_tags($string);
}
$name = remove_headers($_POST[‘name’]);
$phone = remove_headers($_POST[‘phone’]);
$email = remove_headers($_POST[‘email’]);
$to = “info@mydomain.com”;
$subject = “Newsletter”;
$name = “Name: $name”;
$phone = “Phone: $phone”;
$email = “Email: $email”;
mail($to, $subject, $name, $phone, $email, $headers)
?>
Hi Margreth – it looks as if your code will never reach the mail() function, as it encounters the header() redirection first — this will redirect the page and stop processing the page. Try moving the mailing code inside your else clause.
Matt,
You article was great! I have some limited PHP experience and I have been looking for a simple contact form submission script. My question is, can you combine the mail() PHP with the post to database PHP in one document? How would that look? Would I have to open and close 2 different functions?
I have tried combining the 2 PHP functions into one document, but I get a parse error on line 3 (Parse error: syntax error, unexpected T_VARIABLE in /home/u4/f22designs/html/process_form.php on line 3)
I am new to PHP and I may need some basic help.
Can anyone please help?
The process_php code follows.
<? php
// Pick up the form data and assign it to variables
$first_name = $POST[‘first_name’];
$last_name = $_POST[‘last_name’];
$email = $_POST[‘email’];
$phone = $_POST[‘phone’];
$project_type = $_POST[‘project_type’];
// Build the email (replace the address in the $to section with your own)
$to = ‘chris@f22designs.com’;
$subject = “New message: Quote Request”;
$message = “$first_name said: I need a quote for $project_type”;
$headers = “From: $email”;
// Send the mail using PHPs mail() function
mail($to, $subject, $message, $headers);
// Redirect
header(“Location: thanks.html”);
// Mail header removal
function remove_headers($string) {
$headers = array(
“/to\:/i”,
“/from\:/i”,
“/bcc\:/i”,
“/cc\:/i”,
“/Content\-Transfer\-Encoding\:/i”,
“/Content\-Type\:/i”,
“/Mime\-Version\:/i”
);
if (preg_replace($headers, ‘’, $string) == $string) {
return $string;
} else {
die(‘You think I’m spammy? Spammy how? Spammy like a clown, spammy?’);
}
}
// Open database connection
$conn = mysql_connect(‘localhost’, ‘root’, ‘’);
mysql_select_db(‘f22designs’);
// Data cleaning function
function clean_data($string) {
if (get_magic_quotes_gpc()) {
$string = stripslashes($string);
}
$string = strip_tags($string);
return mysql_real_escape_string($string);
}
// Pick up the cleaned form data
$first_name = clean_data($_POST[‘first_name’]);
$last_name = clean_data($_POST[‘last_name’]);
$email = clean_data($_POST[‘email’]);
$phone = clean_data($_POST[‘phone’]);
$project_type = clean_data($_POST[‘project_type’]);
// Insert data
$query = “INSERT INTO contacts (name, email, topic, comments) …
VALUES (’$first_name’, ‘$last_name’, ‘$email’, ‘$phone’, ‘$project_type’)”;
mysql_query($query);
// Close connection
mysql_close($conn);
?>
@Chris:
The “unexpected T_VARIABLE” error is probably caused by having pasted a snippet of code that had smart quotes — make sure that the code you are using only has the single or double quotes that you can type using the keyboard.
The code as you have posted it won’t work, but all you have to do to fix it is to move the header() call to the bottom (below the mysql_close()) and it should do exactly what you wanted.
Thanks Matt, I will give it a whirl.
Got the form to redirect OK, and sends me mail, but the mail is blank :(
Here’s the code:
<?php
// Pick up the form data and assign it to variables
$name = $_POST[‘Contact Name’];
$address = $_POST[‘Contact Address’];
$number = $_POST[‘Contact Number’];
$preffered = $_POST[‘Preffered Contact’];
$email = $_POST[‘e-mail’];
$position = $_POST[‘Position’];
$details = $_POST[‘Event Details’];
$organisation = $_POST[‘Oranisation Name’];
$address = $_POST[‘Event Address’];
$situation = $_POST[‘Indoors, outdoors’];
$eventtime = $_POST[‘Time of Event’];
$start = $_POST[‘Start Time’];
$eventtype = $_POST[‘Type of Event’];
$competitors = $_POST[‘Number of competitors’];
$advertising = $_POST[‘Program Advertsing’];
$PA = $_POST[‘PA system’];
$power = $_POST[‘Mains power’];
$hear = $_POST[‘Hear about us’];
// Build the email (replace the address in the $to section with your own)
$to = ‘alex@imagebase.co.za’;
$subject = “Booking Form”;
$headers = “From: $email”;
// Send the mail using PHPs mail() function
mail($to, $subject, $message, $headers);
// Redirect
header(“Location: success.html”);
?>
any ideas?
Hi Alex – your mail() function is using the variable $message for the body of the email, but I can’t see where you’re populating that variable with any data?
Hm, I used code identical to Aziel above and get this warning (all works ok though). Ideas?
Warning: Cannot modify header information – headers already sent by (output started at blah/process-form.php:2) in blah/process-form.php on line 46
$Body = “”; $Body .= “Name: “; $Body .= $Name; $Body .= “\n”;
Chris – it’s difficult to say without seeing your code, but that error means that something has caused some output to be sent to the browser before the header() function is called, probably on line 46.
Hi
This is the code. Line 46 doesn’t exist as I changed it round since then, but it may be obvious to you?
<?php
//make sure form is used
if (stristr($_SERVER[‘HTTP_RERERER’], ‘form.html’) === FALSE) die(‘No direct script access permitted’);
// Mail header removal
function remove_headers($string) {
$headers = array(
“/to\:/i”,
“/from\:/i”,
“/bcc\:/i”,
“/cc\:/i”,
“/Content\-Transfer\-Encoding\:/i”,
“/Content\-Type\:/i”,
“/Mime\-Version\:/i”
);
$string = preg_replace($headers, ‘’, $string);
return strip_tags($string);
}
// Pick up the cleaned form data
$name = remove_headers($_POST[‘name’]);
$email = remove_headers($_POST[‘email’]);
$topic = remove_headers($_POST[‘topic’]);
$comments = remove_headers($_POST[‘comments’]);
//BUILD EMAIL
$to = ‘blah…’;
$subject = “New Message: $topic”;
$message = “$name said: $comments”;
$headers = “From: $email”;
// Send the mail using PHPs mail() function
mail($to, $subject, $message, $headers);
// Redirect to give success message.
header(“Location: success.html”);
?>
There isn’t anything obviously wrong there, Chris – what line number does the error occur on now (and which line is that, as your formatting isn’t clear in the comment)?
I dont know if this reply page is still open but I have been trying to get this form to work and all goes smooth except for that I dont receive the information from the form, the page gets redirected and I also added the suggested code to see if there is a problem with sending but that does not pick up any errors?
I have copied the code below. What is it I need to look at to try and solve the problem?
<?php
// Process only from my form (change if html has different name)
if (stristr($_SERVER[‘HTTP_REFERER’], ‘form.html’) === FALSE) die(‘No direct script access permitted’);
// Mail header removal
function remove_headers($string) { $headers = array( ‘/to\:/i’, ‘/from\:/i’, ‘/bcc\:/i’, ‘/cc\:/i’, ‘/Content\-Transfer\-Encoding\:/i’, ‘/Content\-Type\:/i’, ‘/Mime\-Version\:/i’ ); $string = preg_replace($headers, ‘’, $string); return strip_tags($string);
}
// Pick up the form data and assign it to variables
$name = $_POST[‘name’];
$email = $_POST[‘email’];
$topic = $_POST[‘topic’];
$comments = $_POST[‘comments’];
// Build the email (replace the address in the $to section with your own)
$to = “private@email.com”;
$subject = “New message: $topic”;
$message = “$name said: $comments”;
$headers = “From: $email”;
// Send the mail using PHPs mail() function
if (mail($to, $subject, $message, $headers)) {
// Successful send
header(“Location: yippee.html”);
} else {
// Function call failed
die(“There was a problem sending your email.”);
}
?>
I’m switching off comments now, as this article is attracting a lot of spammers. If anyone is looking for friendly, helpful advice on PHP development, I recommend trying the Sitepoint forums.


You can use this
feed to keep up with the comments made on this article.