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 validationThe 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.
2 Comments
captcha is better than form token.
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.