Validating User Input
Validating user input in PHP
By Kevin Waterson
Contents
- Never trust user Input.
- Where do I begin Checking?
- How do I check my strings?
- How do I validate a number?
- Can I validate an email address?
- What about database security?
- Why do I get Notice: Undefined index:?
- Putting it all together - A working example
Never trust user input.
This article is an attempt to show how input from web based forms can be dealt with safely. The first and most fundamental rule in security is 'NEVER TRUST USER INPUT'. Just in case this is not entirely clear, lets go over it again.. 'NEVER TRUST USER INPUT'. Whether it be by user stupidity or an attack from a malicious user, every piece of information you get from userland should be treated as suspect. Only by vigilantly adhering to this policy will your scripts and information be secure.
This article is an attempt to show how input from web based forms can be dealt with safely. The first and most fundamental rule in security is 'NEVER TRUST USER INPUT'. Just in case this is not entirely clear, lets go over it again.. 'NEVER TRUST USER INPUT'. Whether it be by user stupidity or an attack from a malicious user, every piece of information you get from userland should be treated as suspect. Only by vigilantly adhering to this policy will your scripts and information be secure.
The second rule deals with a legacy from earlier PHP versions.
Never, ever, ever, ever use register globals
PHP now has super globals and it is HIGHLY recommended to use them.
This article is by no means a complete security run down, simply and explanation of a single facet of securing your scripts. At the absolute least, variables must be checked for type and length. In this tutorial we will take some user input from a web form, put it into a database and email the user a message thanking them for their input.
Never, ever, ever, ever use register globals
PHP now has super globals and it is HIGHLY recommended to use them.
This article is by no means a complete security run down, simply and explanation of a single facet of securing your scripts. At the absolute least, variables must be checked for type and length. In this tutorial we will take some user input from a web form, put it into a database and email the user a message thanking them for their input.
Where do I begin checking?
At the beginning of course. The origin of all your input is usually the form on your page. You can begin here by having a maxlength attribute in your input fields. This will save the honest users from their own stupidity. Lets put together a simple HTML form.
At the beginning of course. The origin of all your input is usually the form on your page. You can begin here by having a maxlength attribute in your input fields. This will save the honest users from their own stupidity. Lets put together a simple HTML form.
<h3>* denotes required field!</h3>
<form action="<?php echo $_SERVER['PHP_SELF'];?>" method="post">
<p>
<label for="name">Name</label>
<input id="name" type="text" name="userName" maxlength="25" />*<br />
<label for="address">Address</label>
<input id="address" type="text" name="userAddress" maxlength="100" /><br />
<label for="city">City</label>
<input id="city" type="text" name="userCity" maxlength="25" /><br />
<label for="zip">Zip</label>
<input id="zip" type="text" name="userZip" maxlength="5" /><br />
<label for="email">Email</label>
<input id="email" type="text" name="userEmail" maxlength="50" />*<br />
<label for="submit">Submit</label>
<input id="submit" type="submit" value="Mail It!" /><br />
</p>
</form>
With the maxlength set in our form, we can prevent most users from accidentally entering strings of 2 megabytes or something silly. Always check for string length. As you can see in our form the action is $_SERVER['PHP_SELF'] so that it posts to itself. We have 5 input fields named:
- userName
- userAddress
- userCity
- userZip
- userEmail
>From the name of the input fields we build our super global $_POST array. All of the inputs are of the type text. This means they will give us a string in the $_POST array of the same name. ie:
- userName becomes $_POST['userName']
- userAddress becomes $_POST['userAddress']
- userCity becomes $_POST['userCity']
- userZip becomes $_POST['userZip']
- userEmail becomes $_POST['userEmail']
With this in mind, we can now begin to check them individually for content.
How do I check my strings?
As PHP is loosely typed, all your information from the form will be a string. So we know what to expect. However, some of the fields we know will numbers. like the phone number and the zip code. We will need to keep this in mind as we can check that numbers have been submitted. For now, we will check the inputs that we know are strings. We know from our maxlengths the maximum length our string should be.
Why should I check for length if we have set the maxlength in our form already?
A valid question and I am glad you asked.
The purpose of validating the user input is that a malicious user may use a form on their own machine to submit to your machine. The form on the remote machine may not have simple checking and may submit strings of the wrong type or length to your machine. Consider this form:
As PHP is loosely typed, all your information from the form will be a string. So we know what to expect. However, some of the fields we know will numbers. like the phone number and the zip code. We will need to keep this in mind as we can check that numbers have been submitted. For now, we will check the inputs that we know are strings. We know from our maxlengths the maximum length our string should be.
Why should I check for length if we have set the maxlength in our form already?
A valid question and I am glad you asked.
The purpose of validating the user input is that a malicious user may use a form on their own machine to submit to your machine. The form on the remote machine may not have simple checking and may submit strings of the wrong type or length to your machine. Consider this form:
<form action="http://example.com/yourfile.php" method="post">
Name: <input type="text" name="userName" /><br />
Address: <input type="text" name="userAddress" /><br />
City: <input type="text" name="userCity" /><br />
Zip: <input type="text" name="userZip" /><br />
Email: <input type="text" name="userEmail" /><br />
<input type="submit" value="Mail It!">
</form>
With the form above, the malicious user can now submit to yourfile.php and have whatever values they want for the input fields. So, we must check.
We begin to check our variables for content and length with a simple function we will call sanityCheck(). This function takes three params.
$type - the type of variable, can be bool, float, numeric, string, array, or object
$string - The string from the form
$length - The maximum length of the string
We begin to check our variables for content and length with a simple function we will call sanityCheck(). This function takes three params.
$type - the type of variable, can be bool, float, numeric, string, array, or object
$string - The string from the form
$length - The maximum length of the string
<?php
/**
* This function can be used to check the sanity of variables
*
* @access private
*
* @param string $type The type of variable can be bool, float, numeric, string, array, or object
* @param string $string The variable name you would like to check
* @param string $length The maximum length of the variable
*
* return bool
*/
function sanityCheck($string, $type, $length){
// assign the type
$type = 'is_'.$type;
if(!$type($string))
{
return FALSE;
}
// now we see if there is anything in the string
elseif(empty($string))
{
return FALSE;
}
// then we check how long the string is
elseif(strlen($string) > $length)
{
return FALSE;
}
else
{
// if all is well, we return TRUE
return TRUE;
}
}?>
With this function we can now create some variables to use within our script. We also need to check that each of our variable is set. We could do this individually with isset for each variable as we check with sanityCheck() like this:
<?php
// check the POST variable userName
if(isset($_POST['userName']) && sanityCheck($_POST['userName'], 'string', 25) != FALSE)
{
$userName = $_POST['userName'];
}
?>
Alternatively, you could use isset()'s ability to take multiple variables and check them all in one sweep by creating a function to do it for us. It might look something like this:
<?php
// check ALL the POST variables
function checkSet(){
return isset($_POST['userName'], $_POST['userAddress'], $_POST['userCity'], $_POST['userZip'], $_POST['userEmail']);
}?>
Now our Code might look something like this:
<?php
// check all our variables are set
if(checkSet() != FALSE)
{
// check the POST variable userName is sane, and is not empty
if(empty($_POST['userName'])==FALSE && sanityCheck($_POST['userName'], 'string', 25) != FALSE)
{
$userName = $_POST['userName'];
}
else
{
echo 'Username is not set';
exit();
}
}
else
{
// this will be the default message if the form accessed without POSTing
echo '<p>Please fill in the form above</p>';
}?>
Lets look at what we have done here. First we used our checkSet function to be sure all the variables were set. When the form page is first accessed, these variables are not set, so the default message
Please fill in the form above
is displayed. This also helps should a malicious user try to use there own form without a variable and so it will throw an error saying Undefined Index and will give the path of the filename. Not a good thing security wise. If all the variable have been set, then we can begin to check the sanity of of the variables using our sanityCheck() function. The first variable we have checked is the userName variable. This field is a required field in our form, so we check it with empty function to be sure that it has a value, i.e. It is not empty or zero. If userName is empty the script echoes and error message and exit()'s the script. Lets continue with some more of our fields. We do the same, exept we do not need to use the empty function to check for non-required fields.
Please fill in the form above
is displayed. This also helps should a malicious user try to use there own form without a variable and so it will throw an error saying Undefined Index and will give the path of the filename. Not a good thing security wise. If all the variable have been set, then we can begin to check the sanity of of the variables using our sanityCheck() function. The first variable we have checked is the userName variable. This field is a required field in our form, so we check it with empty function to be sure that it has a value, i.e. It is not empty or zero. If userName is empty the script echoes and error message and exit()'s the script. Lets continue with some more of our fields. We do the same, exept we do not need to use the empty function to check for non-required fields.
<?php
// check all our variables are set
if(checkSet() != FALSE)
{
// check the POST variable userName is sane, and is not empty
if(empty($_POST['userName'])==FALSE && sanityCheck($_POST['userName'], 'string', 25) != FALSE)
{
$userName = $_POST['userName'];
}
else
{
echo 'Username is not set';
exit();
}
// here we test for the sanity of userAddress, we dont need to stop the
// the script if it is empty as it is not a required field.
if(sanityCheck($_POST['userAddress'], 'string', 100) != FALSE)
{
$userAddress = $_POST['userAddress'];
}
// here we test for the sanity of userCity, we dont need to stop the
// the script if it is empty as it is not a required field.
if(sanityCheck($_POST['userCity'], 'string', 25) != FALSE)
{
$userCity = $_POST['userCity'];
}
}
else
{
// this will be the default message if the form accessed without POSTing
echo '<p>Please fill in the form above</p>';
}?>
How do I validate a number.
Ok, now we come to something a little different, the next variable we must deal with is the userZip. This will have a value which is numerical. Whilst we still need to do our basic sanity check, we also need to check that the number is not greater than 5 digits and be sure a mailicious user does not try to put in something like minus ten or something silly. So, we need a function to check for this also. In our sanityCheck function we set the type to numeric which will check that the value is a number using is_numeric. You can use this function to check the numeric values.
Ok, now we come to something a little different, the next variable we must deal with is the userZip. This will have a value which is numerical. Whilst we still need to do our basic sanity check, we also need to check that the number is not greater than 5 digits and be sure a mailicious user does not try to put in something like minus ten or something silly. So, we need a function to check for this also. In our sanityCheck function we set the type to numeric which will check that the value is a number using is_numeric. You can use this function to check the numeric values.
<?php
// check number is greater than 0 and $length digits long
// returns TRUE on success
function checkNumber($num, $length){
if($num > 0 && strlen($num) == $length)
{
return TRUE;
}
}?>
With this little function we can now also check our numbers are correct for our use. Lets continue again:
<?php
// check all our variables are set
if(checkSet() != FALSE)
{
// check the POST variable userName is sane, and is not empty
if(empty($_POST['userName'])==FALSE && sanityCheck($_POST['userName'], 'string', 25) != FALSE)
{
$userName = $_POST['userName'];
}
else
{
echo 'Username is not set';
exit();
}
// here we test for the sanity of userAddress, we dont need to stop the
// the script if it is empty as it is not a required field.
if(sanityCheck($_POST['userAddress'], 'string', 100) != FALSE)
{
$userAddress = $_POST['userAddress'];
}
else
{
$userAddress = '';
}
// here we test for the sanity of userCity, we dont need to stop the
// the script if it is empty as it is not a required field.
if(sanityCheck($_POST['userCity'], 'string', 25) != FALSE)
{
$userCity = $_POST['userCity'];
}
else
{
$userCity = '';
}
// check the sanity of the number and that it is greater than zero and 5 digits long
if(sanityCheck($_POST['userZip'], 'numeric', 5) != FALSE && checkNumber($_POST['userZip'], 5) == TRUE)
{
$userZip = $_POST['userZip'];
}
else
{
$userZip='';
}
}
else
{
// this will be the default message if the form accessed without POSTing
echo '<p>Please fill in the form above</p>';
}?>
>From the code we now have, we see that the userZip must be numerical and must be 5 digits long. If it is not, it is of no consequence as it is not a required field and can remain blank.
Can I validate an email address?
This brings us to our last variable. It requires a valid email address. To achieve this we use need to validate it not only with our basic sanity checks but with a regular expression or 'regex'. Many people try to avoid them, but for this sort of thing they are the right tool for the job. Here is a function that uses a regex to validate an email address. If you find you have a better regex for email, send it, until then....
This brings us to our last variable. It requires a valid email address. To achieve this we use need to validate it not only with our basic sanity checks but with a regular expression or 'regex'. Many people try to avoid them, but for this sort of thing they are the right tool for the job. Here is a function that uses a regex to validate an email address. If you find you have a better regex for email, send it, until then....
<?phpfunction checkEmail($email){
return preg_match('/^\S+@[\w\d.-]{2,}\.[\w]{2,6}$/iU', $email) ? TRUE : FALSE;
}?>
To put it all together, we can now validate and assign all of our form data like this:
<?php
// check all our variables are set
if(checkSet() != FALSE)
{
// check the POST variable userName is sane, and is not empty
if(empty($_POST['userName'])==FALSE && sanityCheck($_POST['userName'], 'string', 25) != FALSE)
{
$userName = $_POST['userName'];
}
else
{
echo 'Username is not set';
exit();
}
// here we test for the sanity of userAddress, we dont need to stop the
// the script if it is empty as it is not a required field.
if(sanityCheck($_POST['userAddress'], 'string', 100) != FALSE)
{
$userAddress = $_POST['userAddress'];
}
else
{
$userAddress = '';
}
// here we test for the sanity of userCity, we dont need to stop the
// the script if it is empty as it is not a required field.
if(sanityCheck($_POST['userCity'], 'string', 25) != FALSE)
{
$userCity = $_POST['userCity'];
}
else
{
$userCity = '';
}
// check the sanity of the number and that it is greater than zero and 5 digits long
if(sanityCheck($_POST['userZip'], 'numeric', 5) != FALSE && checkNumber($_POST['userZip'], 5) == TRUE)
{
$userZip = $_POST['userZip'];
echo $userZip;
}
else
{
$userZip='';
}
if(sanityCheck($_POST['userEmail'], 'numeric', 5) != FALSE && checkEmail($_POST['userEmail']) != FALSE)
{
$userEmail = $_POST['userEmail'];
}
else
{
echo 'Invalid Email Address Supplied';
exit();
}
}
else
{
// this will be the default message if the form accessed without POSTing
echo '<p>Please fill in the form above</p>';
}?>
What about Database security
With this much complete, we now have valid variables. That means they are within the confines of what we need. Almost. For our purposes here, we are going to use these variables to insert into a MySQL database. For this we will need some extra check if we wish to keep our database intact. The correct tool for this is mysql_real_escape_string. Without this small piece of checking, your SQL queries are open to SQL injection attacks. This can be anything from by-passing access controls to executing system commands. This is by no means confined to MySQL, PostgreSQL and others are just as vulnerable. For an excellent description of this consult The PHP Manual section on SQL Injection So, before we insert our data, lets put together a little table in the test database of mysql. Our SQL will look like this:
With this much complete, we now have valid variables. That means they are within the confines of what we need. Almost. For our purposes here, we are going to use these variables to insert into a MySQL database. For this we will need some extra check if we wish to keep our database intact. The correct tool for this is mysql_real_escape_string. Without this small piece of checking, your SQL queries are open to SQL injection attacks. This can be anything from by-passing access controls to executing system commands. This is by no means confined to MySQL, PostgreSQL and others are just as vulnerable. For an excellent description of this consult The PHP Manual section on SQL Injection So, before we insert our data, lets put together a little table in the test database of mysql. Our SQL will look like this:
CREATE TABLE people (
userName VARCHAR( 25 ) NOT NULL ,
userAddress VARCHAR( 100 ) NOT NULL ,
userCity VARCHAR( 25 ) NOT NULL ,
userZip INT( 5 ) NOT NULL ,
userEmail VARCHAR( 50 ) NOT NULL
);
To put this into a database is a trivial matter with
<?php// Connect$link = @mysql_connect('localhost', 'mysql_user', 'mysql_password');
if (!$link)
{
die('Not connected : ' . mysql_error());
}
// make foo the current db$db_selected = mysql_select_db('test', $link);
if (!$db_selected)
{
die ("Database not selected : " . mysql_error());
}
// Query$query = sprintf("INSERT INTO people (userName, userAddress, userCity, userZip, userEmail)
VALUES( '%s', '%s','%s','%s','%s')",
mysql_real_escape_string($userName),
mysql_real_escape_string($userAddress),
mysql_real_escape_string($userCity),
mysql_real_escape_string($userZip),
mysql_real_escape_string($userEmail));
// run the queryif(!mysql_query($query)
{
echo 'Query failed '.mysql_error();
exit();
}
else
{
$subject = 'Submission';
$msg= 'Thank you for submitting your information';
mail($userEmail,$subject,$msg,
"From: $userEmail\nReply-To: $userEmail\nX-Mailer: PHP/" . phpversion());
}?>
Why do I get Notice: Undefined index:?
This is due to you not checking if a variable is set. This should be done with the isset()function. You should not use empty() to check if a variable is set because empty() will return FALSE if the variable is zero. My personal preference is to check ALL variables with isset() and then check any variables I wish to be sure have a value with empty().
This is due to you not checking if a variable is set. This should be done with the isset()function. You should not use empty() to check if a variable is set because empty() will return FALSE if the variable is zero. My personal preference is to check ALL variables with isset() and then check any variables I wish to be sure have a value with empty().
Putting it all together.
Here is a complete script using the information an examples from above. You must first create the database test if it does not exist, and create the table people using the SQL example above.
Here is a complete script using the information an examples from above. You must first create the database test if it does not exist, and create the table people using the SQL example above.
<!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" lang="en" xml:lang="en">
<head>
<title>Validating User Input.</title>
<style type="text/css">
<!--
label,input {
display: block;
width: 150px;
float: left;
margin-bottom: 10px;
}
label {
text-align: right;
width: 75px;
padding-right: 20px;
}
br {
clear: left;
}
-->
</style>
</head>
<body>
<h3>* denotes required field!</h3>
<form action="<?php echo $_SERVER['PHP_SELF'];?>" method="post">
<p>
<label for="name">Name</label>
<input id="name" type="text" name="userName" maxlength="25" />*<br />
<label for="address">Address</label>
<input id="address" type="text" name="userAddress" maxlength="100" /><br />
<label for="city">City</label>
<input id="city" type="text" name="userCity" maxlength="25" /><br />
<label for="zip">Zip</label>
<input id="zip" type="text" name="userZip" maxlength="5" /><br />
<label for="email">Email</label>
<input id="email" type="text" name="userEmail" maxlength="50" />*<br />
<label for="submit">Submit</label>
<input id="submit" type="submit" value="Mail It!" /><br />
</p>
</form>
<?php
/**
* This function can be used to check the sanity of variables
*
* @access private
*
* @param string $type The type of variable can be bool, float, numeric, string, array, or object
* @param string $string The variable name you would like to check
* @param string $length The maximum length of the variable
*
* return bool
*/
function sanityCheck($string, $type, $length){
// assign the type
$type = 'is_'.$type;
if(!$type($string))
{
return FALSE;
}
// now we see if there is anything in the string
elseif(empty($string))
{
return FALSE;
}
// then we check how long the string is
elseif(strlen($string) > $length)
{
return FALSE;
}
else
{
// if all is well, we return TRUE
return TRUE;
}
}
/**
* This function if the $_POST vars are set
*
* @access private
*
* return bool
*/
function checkSet(){
return isset($_POST['userName'], $_POST['userAddress'], $_POST['userCity'], $_POST['userZip'], $_POST['userEmail']);
}
/**
* This function checks a number is greater than zero
* and exactly $length digits. returns TRUE on success.
*
* @access private
*
* @param int $num The number to check
* @param int $length The number of digits in the number
*
* return bool
*/
function checkNumber($num, $length){
if($num > 0 && strlen($num) == $length)
{
return TRUE;
}
}
/**
* This function checks if an email address in a valid format
*
* @access private
*
* @param string $email The email address to check
*
* return bool
*/
function checkEmail($email){
return preg_match('/^\S+@[\w\d.-]{2,}\.[\w]{2,6}$/iU', $email) ? TRUE : FALSE;
}
// check all our variables are set
if(checkSet() != FALSE)
{
// check the POST variable userName is sane, and is not empty
if(empty($_POST['userName'])==FALSE && sanityCheck($_POST['userName'], 'string', 25) != FALSE)
{
//If all is well we can assign the value of POST field to a variable
$userName = $_POST['userName'];
}
else
{
// if all is not well, we echo an error and exit the script
echo 'Username is not set';
exit();
}
// here we test for the sanity of userAddress, we dont need to stop the
// the script if it is empty as it is not a required field.
if(sanityCheck($_POST['userAddress'], 'string', 100) != FALSE)
{
// if all is well we assign the userAddress to a variable
$userAddress = $_POST['userAddress'];
}
else
{
// if all is not well, we simply give the userAddress a blank value
$userAddress = '';
}
// here we test for the sanity of userCity, we dont need to stop the
// the script if it is empty as it is not a required field.
if(sanityCheck($_POST['userCity'], 'string', 25) != FALSE)
{
// again we assign the POSTed value to a variable
$userCity = $_POST['userCity'];
}
else
{
// or give the variable a blank value
$userCity = '';
}
// check the sanity of the number and that it is greater than zero and 5 digits long
if(sanityCheck($_POST['userZip'], 'numeric', 5) != FALSE && checkNumber($_POST['userZip'], 5) == TRUE)
{
// if the number is valid, we assign it to a variable
$userZip = $_POST['userZip'];
}
else
{
// or give the variable a blank value
$userZip='';
}
// check the sanity of the userEmail sent from the form
if(sanityCheck($_POST['userEmail'], 'string', 5) != FALSE && checkEmail($_POST['userEmail']) != FALSE)
{
// if the checks are ok for the email we assign the email address to a variable
$userEmail = $_POST['userEmail'];
}
else
{
// if all is not well we echo an error message
echo 'Invalid Email Address Supplied';
// and exit the script
exit();
}
// Connect to the MySQL
$link = mysql_connect('localhost', 'mysql_user', 'mysql_password');
if (!$link)
{
die('Not connected : ' . mysql_error());
}
// select test as the current db
$db_selected = mysql_select_db('test', $link);
if (!$db_selected)
{
die ("Database not selected : " . mysql_error());
}
// Build our query here and check each variable with mysql_real_escape_string()
$query = sprintf("INSERT INTO people (userName, userAddress, userCity, userZip, userEmail)
VALUES( '%s', '%s','%s','%s','%s')",
mysql_real_escape_string($userName),
mysql_real_escape_string($userAddress),
mysql_real_escape_string($userCity),
mysql_real_escape_string($userZip),
mysql_real_escape_string($userEmail));
// run the query
if(!mysql_query($query))
{
echo 'Query failed '.mysql_error();
exit();
}
else
{
// if all is well we mail off a little thank you email. We know it is
// safe to do so because we have validated the email address.
$subject = 'Submission';
$msg= 'Thank you for submitting your information';
if(!mail($userEmail,$subject,$msg, "From: $userEmail\nReply-To: $userEmail\nX-Mailer: PHP/" . phpversion()))
{
echo 'Unable to send confirmation mail';
}
else
{
echo 'Thank you for your submission, a confirmation email has bee sent to '.$userEmail;
}
}
}
else
{
// this will be the default message if the form accessed without POSTing
echo '<p>Please fill in the form above</p>';
}
?>
</body>
</html>