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

Live demo | Full source code

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('rn', '<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: $daten"; fwrite($fh, $string);
		$string = "I.P: $ipn"; fwrite($fh, $string);
		$string = "Host: $hostn"; fwrite($fh, $string);
		$string = "Location: $locationn"; fwrite($fh, $string);
		$string = "Error: $errorn"; fwrite($fh, $string);
		$string = "Info: $infon------------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: $daten"; fwrite($fh, $string);
		$string = "I.P: $ipn"; fwrite($fh, $string);
		$string = "Host: $hostn"; fwrite($fh, $string);
		$string = "Location: $locationn"; fwrite($fh, $string);
		$string = "Error: $errorn"; fwrite($fh, $string);
		$string = "Info: $infon------------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 . "rn";
	$headers .= "Reply-To: ". $mailemail . "rn";
	$headers .= "MIME-Version: 1.0rn";
	$headers .= "Content-Type: text/html; charset=ISO-8859-1rn";

	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.RocketMill.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="https://www.rocketmill.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.