• Received IE6 Countdown shirts thanks to Microsoft

    Just over a year ago Microsoft created iecountdown.com to raise awareness and to get people to upgrade their IE6 browsers to a modern browser. We were one of the first companies who supported this initiative and as a result we were contacted by the…

    Posted in Google Read More
  • How to write branding guidelines [INFOGRAPHIC]

    On the eve of our upcoming re-brand, I’ve decided to collate some information together into an infographic that will help me (and hopefully you) put together a concise and comprehensive branding guidelines document. Considering a balance of…

    Posted in Google Read More
  • Why Amazon.com got hit by Google Panda in the UK

    In a recent post titled Search Quality Highlights: 40 Changes for February, Google’s Amit Singhal wrote about some of the latest changes Google has introduced to its different algorithms including an amendment to its previously known Panda upda…

    Posted in Google Read More

Secure PHP form

Posted on August 9, 2011 by under Tips And Tutorials

The most common attack vector on any website is the humble comment form.

Client-side validation, such as jQuery, goes some way to preventing any potentially dangerous data getting submitted but is not a guaranteed safeguard.

For this reason some form of server-side validation is required.

There are many types of attack but all can be prevented by following a few simple rules:

  • Only allow expected data
  • Remove any unwanted data
  • Log any serious issues
  • Make sure the user is genuine

It is good practice to only allow data from form fields we expect, so let’s define them now with a few other variables to save time later on.

// Dummy MySQL connection for testing purposes
// mysql_connect("localhost", "root", "") or die(mysql_error('Error connecting to MySQL'));

session_start(); // Session for storing data
error_reporting(0); // Disable PHP errors (hints for bad guys!). We will show and log our own

$admin_email = 'mradamdavies@twitter.com'; // Where will the emails be sent?
$email_prefix = '[sForm Comment]: '; // Text before the email subject
$post_whitelist = array('token','name','number','email','subject','message'); // List of accepted $_POST variables

We also need to remove any unwanted data submitted only from form fields we are expecting.

This aims at stopping any injection attacks and cross-site scripting.

So let’s create some simple functionsto handle this.

function secure_string($string) { // Remove all unwanted characters from a string
	$string = strip_tags($string);
	$string = htmlspecialchars($string, ENT_QUOTES);
	$string = trim($string);
	if (get_magic_quotes_gpc()) { $string = stripslashes($string); }
	$string = mysql_real_escape_string($string);
	return $string;
}

function clean_message($string){ // Clean submitted message
	$string = mysql_real_escape_string($string);
	$string = str_replace('\r\n', '<br />', $string);
	$string = str_replace("\'", "", $string);
	$string = str_replace('\"', '', $string);
	return '<pre>'.$string.'</pre>';
}

This example assumes a MySQL connection even though the form doesn’t actually use a database.

This is for example purposes so if a database connection isn’t being used, htmlspecialchars would replace mysql_real_escape_string.

Now we have removed any data that could cause problems we should try to check the user is valid.

function generate_token($sForm) { // Generate a random token for the form
	$sForm_token = sha1(uniqid(rand(), true));
	$_SESSION[$sForm.'_token'] = $sForm_token;
	return $sForm_token;
}

function verify_token($sForm) { // Check the generated token matches the one submitted
        if(!isset($_SESSION[$sForm.'_token'])) { return false; }
        if(!isset($_POST['token'])) { return false; }
        if ($_SESSION[$sForm.'_token'] !== $_POST['token']) { return false; }
        return true;
}

$sForm_token = generate_token('sForm'); // Generate a token for later validation

The above code will generate and validate a token so we know the form and user are doing what we expect.
Creating a simple log if anything goes wrong after the above validation is good practice.
So let’s grab the user’s I.P. address, page the form was submitted and generate a log file.

function usersip() { // Get the users I.P for logging purposes
    if (!empty($_SERVER['HTTP_CLIENT_IP'])) { $ip = $_SERVER['HTTP_CLIENT_IP']; }
    elseif (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { $ip = $_SERVER['HTTP_X_FORWARDED_FOR']; }
    else { $usersip = $_SERVER['REMOTE_ADDR']; }
    return $usersip;
}

function current_page() { // Get current page for logging purposes
    $pageURL = 'http://';
    if ($_SERVER["SERVER_PORT"] != "80") { $pageURL .= $_SERVER["SERVER_NAME"].":".$_SERVER["SERVER_PORT"].$_SERVER["REQUEST_URI"]; }
    else { $pageURL .= $_SERVER["SERVER_NAME"].$_SERVER["REQUEST_URI"]; }
    return $pageURL;
}

function log_suspect($error, $info) { // Log errors and possible hack attempts!
	$ip = usersip();
	$host = gethostbyaddr($ip);
	$date = date('d M Y h:i:s A'); // Date and time of the error
	$location = current_page();
	$info = secure_string($info); // Data about the error
	$sFormLog = "sformlog.txt"; // Log to a plain text file for increased security over a database
	if(filesize($sFormLog) < 1887436) { // Append to log if under 1.8Mb
		$fh = fopen($sFormLog, 'a') or die('Error opening sForm log file. Please check file permissions.');
		$string = "Date: $date\n"; fwrite($fh, $string);
		$string = "I.P: $ip\n"; fwrite($fh, $string);
		$string = "Host: $host\n"; fwrite($fh, $string);
		$string = "Location: $location\n"; fwrite($fh, $string);
		$string = "Error: $error\n"; fwrite($fh, $string);
		$string = "Info: $info\n------------\n"; fwrite($fh, $string);
		fclose($fh);
	}
	else { // Clear log if over 1.8Mb and start appending again
		$fh = fopen($sFormLog, 'w') or die('Error writing to sForm log file. Please check file permissions.');
		$string = "Date: $date\n"; fwrite($fh, $string);
		$string = "I.P: $ip\n"; fwrite($fh, $string);
		$string = "Host: $host\n"; fwrite($fh, $string);
		$string = "Location: $location\n"; fwrite($fh, $string);
		$string = "Error: $error\n"; fwrite($fh, $string);
		$string = "Info: $info\n------------\n"; fwrite($fh, $string);
		fclose($fh);
	}
}

At 1.8Mb the log file will reset to help prevent flooding and resource overuse during an unintended loop.

Now we can start sending the email with validated and cleaned code.

	// Clean $_POST variables for email and final validation checks
	$mailname = secure_string($_POST['name']);
	$mailnumber = (float) preg_replace('/[^0-9]*/','',$_POST['number']); // Return just numbers or the false flag
		if ($mailnumber==false) { log_suspect('Invalid phone number', 'Cleaned for security');
		echo 'Invalid phone number'; exit(); };
	$mailemail = filter_var($_POST['email'], FILTER_SANITIZE_EMAIL);
	$mailmessage = clean_message($_POST['message']);
	$mailsubject = secure_string($_POST['subject']);

	// Start sending the email
	$to = $admin_email;
	$subject = $email_prefix.$mailsubject;
	$message = '
	<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
	<html xmlns="http://www.w3.org/1999/xhtml">
	<head><title>Email from sForm</title></head>
	<body>
	<h2>Message from sForm</h2>
	<p><strong>Name:</strong> '.$mailname.'<br />
	<strong>Number:</strong> '.$mailnumber .'<br />
	<strong>Email:</strong> '.$mailemail.'<br />
	<strong>Message:</strong> <br />'.$mailmessage .'<br />
	</p></body></html>';

	$headers = "From: " . $mailemail . "\r\n";
	$headers .= "Reply-To: ". $mailemail . "\r\n";
	$headers .= "MIME-Version: 1.0\r\n";
	$headers .= "Content-Type: text/html; charset=ISO-8859-1\r\n";

	if (mail($to, $subject, $message, $headers)) { // Send email if PHP mail is working or log the error
		echo 'Your message has been sent.'; }
	else { log_suspect('PHP mail error', 'Unable to send mail using PHP mail!');
		echo 'Email failed! Please check PHP Mail settings.'; } exit();

The only thing we need now is our form to submit to.

I have included some CSS styling and jQuery validation for usability purposes.

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Secure contact form | www.ElevateLocal.co.uk</title>
<style type="text/css">
	body{font-family:Verdana, Geneva, sans-serif; color:#666;}
	#center{width:350px;margin:5px auto 0 auto;}
	label{ float:left; width:300px; padding:6px 0 6px 0;}
	input{height:24px} textarea{height:75px}
	input, textarea{float:left; width:300px; border:1px solid #CCC; padding:2px; color:#999;}
	form img{float:left; margin:8px 6px 0 0;}
	h2{color:#CDCDCD; margin-bottom:15px; border-bottom:1px solid #CDCDCD}
	.row{height:70px;} .row2{height:77px;}
	#send{clear:left; width:75px; margin:15px 0 0 230px; border:1px solid #36489c}
	#send:hover{border:1px solid #e91d58; cursor:pointer}
	#sent{width:200px; background:#096; display:block; text-align:center; margin:auto auto;
	padding:10px; border:1px solid #CDCDCD; color:#FFF; font-weight:bold}
	.error{ color:#f00; float:left; font-size:12px;}
</style>
<script type="text/javascript" src="http://code.jquery.com/jquery-latest.min.js"></script>
<script src="http://view.jquery.com/trunk/plugins/validate/jquery.validate.min.js" type="text/javascript"></script>
<script type="text/javascript">
$(document).ready(function() {
	$.validator.addMethod("isclean", function(value, element) {
	return this.optional(element) || /^[a-z0-9 ]+$/i.test(value);
}, "Letters, numbers, and spaces only.");
$("#My_sForm").validate({
	rules: {
		name: "required",// simple rule, converted to {required:true}
		number: { number:true, required: true },
		email: { email: true, required: true },
	},
	errorPlacement: function (error, element) {
            element.parents(".row").find(".error").append(error);
            element.parents(".row2").find(".error").append(error);
	}
  });
});
</script>
</head>
<body>

<div id="center"><a href="http://www.elevatelocal.co.uk/" style="float:right"><img src="./elevate-local.png" alt="Webdesign" /></a>
<form action="./index.php" method="post" id="My_sForm">
    <div class="row"><input type="hidden" name="token" value="<?php echo $sForm_token ?>" />
    <label for="name">Name:</label><div class="error"></div>
    <input type="text" name="name" id="name" maxlength="30" class="required isclean" /></div>

    <div class="row"><label for="number">Number:</label><div class="error"></div>
    <input type="text" name="number" id="number" maxlength="30" class="required name" /></div>

    <div class="row"><label for="email">Email:</label><div class="error"></div>
    <input type="text" name="email" id="email" maxlength="30" class="required email" /></div>

    <div class="row"><label for="subject">Subject:</label><div class="error"></div>
    <input type="text" name="subject" id="subject" maxlength="30" class="required isclean" /></div>

    <div class="row2"><label for="message">Message:</label><div class="error"></div>
    <textarea name="message" id="message" rows="4" cols="20" class="required isclean"></textarea></div>
    <div class="row"><input type="submit" value="Send" id="send" /></div>
</form>

</div>
</body>
</html>

It may seem like overkill to some, but I would rather have too much protection than not enough when handling client data.
This form could be dropped into a CMS and submit to the database without any problems or risk to the existing data or remove the mysql_real_escape_string functions to use without a database connection.

A little about adamdavies …

Adam is a new member of the webdesign team. He has a passion for standards compliant code and the more technical aspects of webdesign.
This post was tagged under: , , ,

2 Comments

  1. huarong on wrote:

    captcha is better than form token.

    • Adam Davies on wrote:

      Captchas and authentication tokens have different uses.
      A captcha is used to prevent spam or automated submissions while an authentication token is used to prevent Cross Site Request Forgery and and other hacks.

      I created a guide to creating a captcha, which can be altered to work with this form:
      Creating a simple captcha

      I will redo this code to include the captcha and session based error storage when I get time.

Want to Contribute? Leave a Comment…

Your email address will not be made public or shared. Inappropriate and irrelevant comments will be removed.