Monday 27 May 2013
Facebook StumbleUpon Twitter Google+ Pin It

Building a Bulletproof Contact Form with PHP


The humble contact form: It’s the cornerstone of nearly every website, from the humble personal blog right up to the corporate megasite—and a billion small business sites in-between. In the early years of operating a website, we were happy to put our shiny new email address out there for anyone to mailto, but the rise of the spammer has made us justifiably wary of publicizing our contact details—enter the contact form.

Setting the Groundwork


form.html containing a standard HTML form to collect the dataOur contact form will actually consist of three separate files:

  • process-form.php which handles the submitted data, and
  • success.html to display a simple thank you message
(It could, of course, be restructured to all live in one single file, but for this article we’ll keep things separate.)
First, we have form.html. Here’s a basic contact form—name, email address, topic, and a textarea for your adoring fans or upset customers to enter comments:
<form action="process-form.php" ...
method="post"> <fieldset>
> <label for="name">Name:</la
<legend>Contact Form</legen dbel> <input type="text" id="name" name="name" />
<input type="text" i
<label for="email"> Email address: </label> d="email" name="email" /> <label for="topic">
<input type="text" id="topic" name=
What do you want to tell us about? </label >"topic" /> <label for="comments"> Your comments: </label>
area> <button type="submit">Send</button> </fieldset> </form>
<textarea id="comments" name="comments" rows="5" cols="30"></tex
t
This is not the time or place to cover the best way to mark up and style forms, but Cameron ‘The Man In Blue’ Adams’s article Fancy Form Design Using CSS on SitePoint is highly recommended.
Our users fill in their personal details and their comments, click Submit, and—through the magic of HTTP —the form is submitted, the text they entered is sent as a POST to the destination we specified in the form’s action attribute, and process-form.php page is loaded.
(Note for PHP beginners: As the following script is solely concerned with receiving the form data and doing something with it, there is no HTML needed. The opening <?php tag tells the server that what follows is PHP. Lines beginning with double forward slash marks are used in PHP for making comments and don’t need to be included when you construct your form.)
<?php
// Pick up the form data and assign it to variables
$name = $_POST['name']; $email = $_POST['email'];
ents']; // Build the ema
$topic = $_POST['topic']; $comments = $_POST['com mil (replace the address in the $to section with your own)
"; $message = "$name said:
$to = 'contact@mysite.com'; $subject = "New message: $topi c$comments"; $headers = "From: $email"; // Send the mail using PHPs mail() function
);
mail($to, $subject, $message, $headers); // Redirect header("Location: success.html
"
For the benefit of any non-PHP experts, let’s briefly look at what this code is doing:
  • First, we created a variable for each of our form fields (the words starting with the dollar sign).
  • We gave each of our variables the relevant value that was entered in the form—when you submit a form, the data is sent as what is called POST data, and we can access that data in PHP using the $_POSTsuperglobal.
  • We created some more variables containing information that is needed to create an email—the address you want the email sent to, the subject, the contents of the message, and who it is from.
  • Finally, we used that information to build an email and send it using PHP’s handy mail() function (for more details on this or any of the other functions used in this article, see php.net—note especially theRequirements section for the mail() function).
  • We redirected the browser to a page that contains a message that confirms that they have successfully submitted the form.
You can change the value of the variables used to create the email to be whatever you like, but did you notice how the subject$message, and $headers variables use the values we collected from the form? By including variables inside double quotation marks, your variables are assigned the values of what the user typed into the contact form, and this is what is sent to you. (Methods other than double quotes can be used—here’s one explanation of the different options available to you. I find that double quotes are clearer, especially for beginners.)
An alternative way of handling feedback might be to insert the data into a database, perhaps so that it can be dealt with later via an admin panel. Easy enough—let’s replace the mail() function (the line starting mail($to… above) with some database magic instead. First, you need to create a database table—in this example, we’ll create a table called submissions inside our database called contact (you can do this either at the MySQL command line on your server, or through an interface such as phpMyAdmin):
CREATE TABLE submissions (
name VARCHAR( 100 ) NOT NULL,
email VARCHAR( 255 ) NOT NULL,
topic VARCHAR( 255 ) NOT NULL,
comments TEXT NOT NULL
);
Then, with the table created, you can add the values of your submitted form’s $name, $email, $topic, and $comments variables to corresponding fields in the database:
// Open database connection
$conn = mysql_connect('localhost', 'username', 'password');
mysql_select_db('contact'); // Insert data
il, topic, comments) ... VALUES ('$name', '$email', '$topic', '$co
$query = "INSERT INTO submissions (name, em amments')"; mysql_query($query); // Close connection
mysql_close($conn);
If you don’t know anything about databases, you may want to ignore this section altogether, but basically what we did by using this code was to:
  • Create a database table to hold submitted data (you only have to do this once).
  • Use PHP’s database functions mysql_connect() and mysql_select_db() to connect to MySQL and select a database named ‘contact’ (you should change this to the name of your own database).
  • Execute a query to insert the data into a table named submissions, which has columns for the name, email, topic, and comments.
  • Close the database connection using mysql_close().
Hey, that was easy, right? Two ways to collect visitor feedback—now we can happily scatter PHP contact forms across the internet, secure in the knowledge that they will always work perfectly.
Well, actually, no. There are several problems with our implementation:
  1. There’s no guarantee that the user entered any data—we don’t want to send a blank email to ourselves or insert an empty row into our table.
  2. We’re assuming that the mail() function or database insert worked properly—what if something went wrong?
  3. We’re assuming that no malicious data has been entered into our form fields by spammers, hackers, or other nasty types.
I’ll leave solutions to the first two problems as an exercise for the reader, as it’s the third issue I want to take a closer look at in this article.

Lions and Tigers and Bears, Oh My!

By entering malicious data into our innocent contact form, hackers or spammers can fool the PHP script into doing something that you didn’t intend it to do. By injecting additional email headers, spammers can cause your contact form to become a spam way-station; or, with just a rudimentary level of MySQL knowledge, your simple database insert can lay bare your private database details. So what do we do to combat those naughty people? Luckily the answer is (fairly) simple—we clean any user input before doing anything with it.

Cleaning Your Data

Clean data (also sometimes referred to as sanitized data) is a term that generally means it has been manipulated to remove anything potentially damaging—but what sort of thing are we talking about, and how exactly can it be removed? Let’s look at avoiding database issues first.
Luckily, cleaning data to avoid MySQL problems is pretty easy, and PHP even provides a handy function to help you do just that. The mysql_real_escape_string() function escapes any potentially troublesome characters, such as quotes and newline characters, by adding backslashes to your data before you use it as part of a SQL statement. For example:
I can't find your 'Shoes' section - I don't know where it is!
becomes
I can\'t find your \'Shoes\' section - I don\'t know where it is!
Why would you need to do that, you may ask? Well, consider a user login form, where you are collecting a username and password to check against your database. Your query might look something like this:
SELECT * FROM users WHERE username = '$_POST['username']' AND password = '$_POST['password']'
Looks fine, but what if someone decides to enter something naughty into your form? Like, say, ”’ OR ‘1’ = ‘1”? Now your query is compromised:
SELECT * FROM users WHERE username = 'hacker' AND PASSWORD = '' OR '1' = '1'
There goes your login security! All data being used in database queries must be treated with suspicion (as beautifully illustrated by XKCD recently).
There is one small gotcha when using the mysql_real_escape_string() function, however—if a setting on your server known as ‘magic quotes’ is switched on, all POST data will already be escaped before it even reaches your page! If we are to avoid double-slashes in that situation, we need to do a tiny bit more cleaning of our data before passing it into the database:
// Open database connection
$conn = mysql_connect('localhost', 'root', '');
// Data cleaning function
) { if (get_magic_quotes_gpc
function clean_data($strin g()) { $string = stripslashes($string); }
/ Pick up the cleaned form data $name = cle
return mysql_real_escape_string($string); } /an_data($_POST['name']); $email = clean_data($_POST['email']);
OST['comments']);
$topic = clean_data($_POST['topic']); $comments = clean_data($_
P
By passing all of our content through a cleaning function before inserting it into the database, we can avoid any SQL errors or security breaches caused by content being submitted via our form. (The database connection must be included first, as it is required by the mysql_real_escape_string() function.) However, there are a few more steps that need to be taken for our form processing script to be considered truly secure.
A form field with HTML entered
If someone submits a chunk of HTML to your form, it won’t be affected by the function above, and will end up in the database as entered—but what happens when it comes out the other end? If the data stored in your database is ever rendered as HTML (in an email or web interface, for example), the user-entered code will be executed—obviously something we want to avoid in our AJAX-powered world—so we must take steps to avoid any markup being submitted:
// Data cleaning function
function clean_data($string) {
if (get_magic_quotes_gpc()) {
ng); } $string = strip_tags($str
$string = stripslashes($str iing); return mysql_real_escape_string($string);
}
In this example, I am using the handy strip_tags() function to simply get rid of any HTML markup that has been submitted. An alternative approach might be to use the less destructive htmlentities(), which simply converts characters such as < and > into their entity equivalents (&lt; and &gt; in this case).

Preventing spam

The other major concern when dealing with online forms is their appropriation and misuse by spammers—by clever manipulation of the values that are submitted, a spammer (or, more likely, a spambot) can rewrite the email headers created by your script and cause it to do far more than send a simple email to you.
To prevent this kind of abuse, a different kind of data cleaning is necessary, as we have to spot and remove those additional email headers. We’ll use the PHP function preg_replace() to carry out a kind of global ‘find and erase’ on the form data to make it safe for mailing:
// Mail header removal
function remove_headers($string) {
$headers = array( "/to\:/i",
"/cc\:/i",
"/from\:/i", "/bcc\:/i", "/Content\-Transfer\-Encoding\:/i",
Version\:/i" ); ret
"/Content\-Type\:/i", "/Mime\ -urn preg_replace($headers, '', $string);
}
Here we have built an array of the headers we want to filter out, then replaced any instances of them with an empty string. Of course, this still means that the spam email will be sent to you, so if you are receiving large volumes of spam you may want to replace that return with a simple die() call to kill the process, or a redirect (if you want to give the spambot the benefit of the doubt)—here’s an example of stopping dead on detection of any spammy data:
function remove_headers($string) {
$headers = array( "/to\:/i",
"/cc\:/i",
"/from\:/i", "/bcc\:/i", "/Content\-Transfer\-Encoding\:/i",
Version\:/i" ); if
"/Content\-Type\:/i", "/Mime\ -(preg_replace($headers, '', $string) == $string) {
m spammy? Spammy ho
return $string; } else { die('You think I 'w? Spammy like a clown, spammy?'); }
}
If you are sending something more than a plain-text email, it would also be a good idea to perform the same markup-stripping as in the case of database inserts—here’s our original function with one extra call, to thestrip_tags() function we used before:
// Mail header removal
function remove_headers($string) {
$headers = array( "/to\:/i",
"/cc\:/i",
"/from\:/i", "/bcc\:/i", "/Content\-Transfer\-Encoding\:/i",
Version\:/i" ); $st
"/Content\-Type\:/i", "/Mime\ -ring = preg_replace($headers, '', $string);
the cleaned form data $name =
return strip_tags($string); } // Pick up remove_headers($_POST['name']);
ail']); $topic = remove_headers($_POST['t
$email = remove_headers($_POST['e mopic']);
= remove_headers($_POST['comments']);
$comments

No comments: