Digital Web Magazine

The web professional's online magazine of choice.

Building a Bulletproof Contact Form with PHP : Comments

By Matthew Pennell

October 15, 2007

Comments

Hrvoje

October 15, 2007 11:30 PM

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?

Matthew Pennell

October 16, 2007 12:51 AM

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’);

Hrvoje

October 16, 2007 1:00 AM

Thank you for the quick and helpfull reply.
My PHP knowledge looks like Swiss chese, and the article filled in nicely.

Stuart Colville

October 16, 2007 4:49 AM

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.

Mike Papageorge

October 16, 2007 5:32 AM

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.

TMaxim

October 16, 2007 5:56 AM

To Mike Papagerge: It’s a good idea add md5 hash to forms. I want to do it on my site too.

Mike Papageorge

October 16, 2007 6:02 AM

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.

Mike Barlund

October 16, 2007 7:38 AM

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?

Matthew Pennell

October 16, 2007 7:47 AM

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).

Derek Allard

October 16, 2007 1:40 PM

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.

Jim Amos

October 16, 2007 6:59 PM

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! :)

Azriel Hayes

October 16, 2007 10:23 PM

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”);

?>

Matthew Pennell

October 17, 2007 6:45 AM

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.

Carsten

October 17, 2007 9:28 AM

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

Geert De Deckere

October 17, 2007 9:32 AM

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.

Matthew Pennell

October 17, 2007 10:28 AM

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.

Azriel Hayes

October 17, 2007 11:23 AM

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. :-)

Walker Hamilton

October 17, 2007 1:53 PM

I fixed the code formatting for preview.

You can use the [at] symbol or bc. to format code.

Patrick

October 17, 2007 5:03 PM

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.

Patrick

October 17, 2007 5:08 PM

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?

Matthew Pennell

October 18, 2007 7:08 AM

@Patrick – magic quotes and mysql_real_escape_string() handle multi-byte character encoding differently (more detailed explanation).

Carsten

October 18, 2007 10:10 AM

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

Carsten

October 18, 2007 10:15 AM

uuuuhh – I’m so stupid – found the problem by myself…;-)

Sorry

.Carsten

Dan

October 18, 2007 10:55 AM

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’);
}

Rob

October 21, 2007 3:41 AM

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

Matthew Pennell

October 21, 2007 6:06 AM

@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.");
}

Mike Cherim

October 23, 2007 7:21 AM

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.

Bjarni Wark

October 23, 2007 5:32 PM

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.

John Elliott

October 29, 2007 8:10 AM

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)

Paul Collins

November 3, 2007 5:37 AM

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.

bruce

November 4, 2007 3:43 AM

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/

Bjarni Wark

November 14, 2007 1:40 PM

Just a quick question, will this form be safe from header_injections?

Matthew Pennell

November 14, 2007 10:56 PM

Bjarni – yes, the section that deals with preg_replacing all the potential headers (To:, Cc:, etc.) deals with that risk.

Bjarni

November 26, 2007 3:53 PM

Cheers Matthew, I will read up on it some more, much appreciated for your time and insights on this PHP contact form.

Carlos Andrade

November 28, 2007 3:19 PM

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

Matthew Pennell

November 29, 2007 1:02 AM

@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.

Rachael

December 18, 2007 6:08 AM

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?

Margreth

January 1, 2008 8:10 AM

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)

?>

Matthew Pennell

January 1, 2008 10:55 PM

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.

Chris Endres

January 11, 2008 3:25 AM

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?

Chris Endres

January 11, 2008 4:26 AM

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);

?>

Matthew Pennell

January 12, 2008 5:17 AM

@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.

Chris

January 12, 2008 9:20 AM

Thanks Matt, I will give it a whirl.

Alex Kilbee

January 22, 2008 1:51 AM

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?

Matthew Pennell

January 22, 2008 11:57 AM

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?

Chris

January 27, 2008 10:59 PM

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”;

Matthew Pennell

January 28, 2008 1:37 AM

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.

Chris

January 28, 2008 10:02 PM

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”);

?>

Matthew Pennell

January 31, 2008 3:13 AM

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)?

Bjarni Wark

February 3, 2008 6:12 PM

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.”);
}
?>

Matthew Pennell

February 18, 2008 2:17 AM

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.

Sorry, comments are closed.

Media Temple

via Ad Packs