Digital Web Magazine

The web professional's online magazine of choice.

Building a Bulletproof Contact Form with PHP

Got something to say?

Share your comments on this topic with other web professionals

In: Articles

By Matthew Pennell

Published on October 15, 2007

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

Our contact form will actually consist of three separate files:

(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>
  <legend>Contact Form</legend>
  <label for="name">Name:</label>
  <input type="text" id="name" name="name" />
  <label for="email">
   Email address:
  </label>
  <input type="text" id="email" name="email" />
  <label for="topic">
    What do you want to tell us about?
  </label>
  <input type="text" id="topic" name="topic" />
  <label for="comments">
    Your comments:
  </label>
  <textarea id="comments" name="comments" rows="5" cols="30"></textarea>
  <button type="submit">Send</button>
 </fieldset>
</form>

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'];
$topic = $_POST['topic'];
$comments = $_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");

For the benefit of any non-PHP experts, let’s briefly look at what this code is doing:

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
$query = "INSERT INTO submissions (name, email, topic, comments) ...
  VALUES ('$name', '$email', '$topic', '$comments')";
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:

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
function clean_data($string) {
  if (get_magic_quotes_gpc()) {
    $string = stripslashes($string);
  }
  return mysql_real_escape_string($string);
}

// Pick up the cleaned form data
$name = clean_data($_POST['name']);
$email = clean_data($_POST['email']);
$topic = clean_data($_POST['topic']);
$comments = clean_data($_POST['comments']);

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()) {
    $string = stripslashes($string);
  }
  $string = strip_tags($string);
  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",
    "/from\:/i",
    "/bcc\:/i",
    "/cc\:/i",
    "/Content\-Transfer\-Encoding\:/i",
    "/Content\-Type\:/i",
    "/Mime\-Version\:/i" 
  ); 
  return 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",
    "/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?');
  }
}

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 the strip_tags() function we used before:

// 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']);

Summary

There are other methods which can be employed to reduce or eliminate contact form spam, ranging from the use of CAPTCHA images or simple questions designed to fox the bots, to clever ‘honeytrap’ techniques involving hidden fields, but by employing the simple techniques described here you can go a long way to preventing the headaches that can come along with operating a simple contact form.

Further reading

Got something to say?

Share your comments  with other professionals (51 comments)

Related Topics: XHTML, Programming, PHP, Interaction Design, HTML, Basics

 

Matthew Pennell works as a senior designer for one of Europe's leading hotel booking websites, writing semantic XHTML, bleeding-edge CSS and JavaScript that usually works. He is the former Managing Editor and former Editor in Chief of Digital Web, and blogs at The Watchmaker Project.

Media Temple

via Ad Packs