Learning how to write a basic web form is beneficial because it teaches you how to handle input. There are many important steps involved including: validation, sanitization, and escaping. Our objective is to create a functional, dynamic form interface for the user. To understand how we should progress through this task, we can analyze how the general flow of the web form interaction should work.
- A user visits the page and a web form is presented.
- The user types values into the required form fields and clicks the submit button.
- The data will be sent either through a GET or POST request to the server, depending on the form configuration.
- Our script processes the data.
- Sanitize the data to correct user input mistakes.
- Validate the data and use escaping where necessary. Record any validation errors.
- At this stage the data is either acceptable or not, indicated by the existence of any validation errors.
- Bad data: we redisplay the form, clearly indicating which fields contained invalid data and how to correct it. Ideally, the form will be repopulated with the previous submission data. When redisplaying data in the form it is important to escape HTML characters.
- Good data: we do something with the data. This may particularly be updating records in a database or sending an email. It is important to use additional data escaping where necessary during this stage.
Step One - Displaying the Form
Lets begin at the top of our requirements and work down. In our example, we will be creating a user registration interface for our make-believe website. To do this, we will need a form that accepts user registration information and code that will deal with that information once submitted. Let us begin with the form.
form.php
<form action="form.php" method="post" id="registration-form">
<ol>
<li>
<label for="registration-form-username">Username:</label>
<input type="text" name="username" id="registration-form-username" />
</li><li>
<label for="registration-form-email">Email:</label>
<input type="text" name="email" id="registration-form-email" />
</li><li>
<label for="registration-real-name">Real Name:</label>
<input type="text" name="real-name" id="registration-real-name" />
</li><li>
<label for="registration-form-password">Password:</label>
<input type="password" name="password" id="registration-form-password" />
</li><li>
<label for="registration-form-password-retype">Retype Password:</label>
<input type="password" name="password-retype" id="registration-form-password-retype" />
</li><li>
<input type="submit" name="registration-form-submit" id="registration-form-submit" value="Register" />
</li>
</ol>
</form>
Step Two & Three - Submitting the Data
This accomplishes the first task: displaying a web form. The second and third task is accomplished by our user, you, so make sure to save the above code as form.php and upload it to your server. As you can experiment, the form at this point will take any data you throw at it and do nothing when you click submit other than reload the page and erase everything you have typed in.
When you click on the submit button, the browser sends the data contained in the form fields to the server in a POST request. You can see that we have specified to use the POST method in our form tag using the method attribute. Another valid method to use is GET. The difference is that GET data is transmitted through the URI in a query string while POST data is transmitted transparently. Which method you use depends on what kind of form you are creating.
Since data sent by GET is transmitted through the URI, you can bookmark it. Every time you visit the bookmark the same GET data will be sent to the server. This is useful for, say, a search form where a user may want to bookmark their exact search. Requests that do not alter the state of your overall application should typically use the GET method.
Data sent by POST is done through a different part of an HTTP request, is not visible to the user, and cannot be bookmarked. This is useful for our situation because the user would have no need to bookmark a user registration; it would only work once if successful anyhow presuming we do not allow duplicate users. Additionally, the URI can only hold so much data because it is restricted in how long it can be. To send larger amounts of data, you will have to use the POST method. This method is ideal for requests that alter the state of your overall application.
Step 4, A Prelude - Why Unknown Input Is Dangerous
Before we get into step four, we need to understand the nature of GET and POST data and how PHP interacts with it.
Our form makes use of the POST method. PHP makes POST data easily available through the $_POST super global. If we had used the GET method, the data would be available in the $_GET super global. Regardless of which of the two super globals we are using, we have to use them cautiously in the same ways.
Before you continue, make sure you are up to speed with how to report errors in PHP.
The mistake many PHP beginners run into is failing to make sure that the key they are using to lookup a value in an array actually exists. If the key does not exist, it results in a notice-level error that reads something like the following.
Notice: Undefined index: foo in /home/eric/localhost/badarrayindex.php on line 5
To clear up terminology, the act of finding a value in an array is either called a lookup or indexing. An index, without the ing suffix, on the other hand, is used synonymously with key.
This undefined index error may occur when we try do something like follows.
$username = $_POST['username'];
$_POST['username'] correlates with the username input we wrote in the web form; after submission, the variable will contain the same data that was typed into the input. However, the only time $_POST['username'] exists is when it was actually submitted and, therefore, specified in the POST data. If the user always uses the form we wrote, there should never be a problem and the error should not happen. Even if they leave the username field blank, $_POST['username'] will still exist and be set to a blank string. When will the error occur? If the user uses a different form that does not have a username field, or submits POST data using his own tool that does not specify a username field, $_POST['username'] will not be set and our script will generate an error if we try to use that index. This can quickly lead to unexpected results.
Let us consider the $_GET super global. It contains the data transmitted through the query string. How easy is it to mess up a query string and submit incorrect data to our scripts? Very. The query string is sitting right there in the address bar ready to be tampered with.
Another issue that many beginner PHP programmers may not be aware of is that POST and GET data can also be submitted as an array. PHP supports a special naming syntax for input field names, or query string key names, that automatically converts into an array structure. This is invaluable for dynamic forms, checkboxes, and radio buttons, to name a few.
Here is an example query string.
http://www.example.com/?data[colours][]=white&data[colours][]=blue&data[item]=sky
When the above is read by PHP, it will populate $_GET with an array. This is what the print_r() of $_GET will look like.
Array
(
[data] => Array
(
[colours] => Array
(
[0] => white
[1] => blue
)
[item] => sky
)
)
As you can see, you have to be ready to handle either a string or an array when dealing with POST and GET data. Someone could have tampered with our POST data and submitted $_POST['username'] as an array. If our script dealt with it like it was a string and took no account for the possibility of an array, we could be opening the door to exploits. The same is obviously true for expecting an array but not being ready to handle a string.
The other danger to unknown input is the data contained within. Even if we had made sure that $_POST['username'] exists and is a string, we still do not know of the exact characters that are contained within the string. To be sure that it is safe and something we are expecting, we have to go through series of validation, sanitization, and escaping steps. For one of many examples, eventually this data may be inserted into a database, so we have to make sure it is escaped properly to avoid SQL injection attacks. Without verifying input data, we can open the door to exploits.
These acts of submitting unexpected POST or GET data can be malicious, trying to trick our code into doing something we don't want it to do by giving it data it may not be able to handle or by not giving it data that it expects. Giving bad input data is very easy for someone who is trying to exploit our script.
The bad input may occur accidentally too, from ourselves even, by misspelling an input name when writing our HTML form or forgetting a piece of GET data we needed to include in a query string in a href attribute. Another example could be a user trying to format a comment with HTML code, except that they are not very good with HTML and forget to close some tags. Consequently, without verifying his comment data at all, when the comment is redisplayed on the website it could break the page's layout.
Always be conscious of possible methods of attack. Accidental bad HTML is just a best-case scenario. Our innocent commenter could have easily been someone dangerous creating a cross-site scripting attack. We should have either stripped the HTML code out completely or verified that it was well-formed and safe.
Thankfully, adapting our code to deal with unknown input and input data is not difficult.
Step Four - Handling & Validating Input
PHP provides a few options to determine if an array key exists before we try using it. Refer to http://php.net if you are unfamiliar with any of the functions or constructs that I talk about throughout the rest of this tutorial. I will include manual page links where for your convenience, where I remember to.
The first option is the isset() construct. Yes, construct, not function. It appears to be a normal function but it is in fact specially defined by PHP to accept variables that may not exist as its arguments.
The second option we could use is the empty() construct. It behaves exactly like isset() except that instead of only returning false if the variable is not set or equal to null, it returns false if the variable can coerce to false. This means we can easily treat not having the field submitted and having a blank string submitted as the same case. Be aware that the string '0' coerces to false and thus an empty() test will fail.
The third option we could use is the function array_key_exists(). This function may be most expressive of what we are trying to do: determine if an array key exists. You are free to use whichever of the functions you like, however. Lets write our validation code using array_key_exists().
$username = '';
$errors = array();
if (array_key_exists('username', $_POST) && is_string($_POST['username'])) {
$username = trim($_POST['username']);
if ($username == '') {
$errors['username'] = 'You must supply a username.';
} elseif (count($username) < 5 || count($username) > 12) {
$errors['username'] = 'Your username must be between 5 and 12 characters.';
}
elseif (!ctype_alnum($username)) {
$errors['username'] = 'Your username can only contain alphanumeric characters.';
}
} else {
$errors['username'] = 'Username not submitted or not submitted in an expected format.';
}
How we wrote the if predicate is important. As evaluation occurs from left to right, the && operator will short-circuit if its first operand is false. This means that if $_POST['username'] does not exist, the is_string() function that uses the non-existing $_POST['username'] will not be executed and thus we will not generate an error. If we had written the operands the other way around, however, is_string() would be evaluated first and would generate an error if $_POST['username'] did not exist. With this aside, lets look at how we carry out rest of the validation.
Before the if statement, we initialize the $username variable to an empty string. Why we would do this will become clear when we deal with re-population of our form. The $errors array will be where we store any errors that occur during validation.
Entering the if statement, the first thing we test is if we can handle the data by ensuring a username was submitted and that it is, in fact, a string and not an array. If either of those requirements fail, we skip to the else block and log an error. In the other case, we know how to validate the data and proceed with doing so in the if block.
We assign $username to the trimmed value of $_POST['username']. Using trim() on input data is common because it is easy for a user to accidentally type extra whitespace that is, of course, invisible to them. This step can be classified as sanitization. Sanitization could be defined as pruning unneeded or harmful data. If we did not use trim(), the user could be getting the "Your username can only contain alphanumeric characters" error because there is an extra space after their otherwise valid username. That could be confusing and frustrating for them.
The next steps begin to validate the specific the input data. Validation exists to reject data altogether in the cases where there is no possible way to treat it correctly. Our requirements for a username, chosen arbitrarily, are that it can only contain alphanumeric characters and it must be between 5 and 12 characters long. The first validation test, seeing if the username is blank, allows us to tell the user explicitly if they forgot to fill it in. The next validation test ensures the username is between 5 and 12 characters. The final validation test ensures the username only contains alphanumeric characters.
If all tests pass, no error is recorded and therefore we can reason that the $username variable must contain good data. If any test fails, the respective error will exist in the $errors array under the username key.
Note: you may have noticed that we did not cover an important validation step in the example. A user registration system typically does not allow two users to have the same username. To validate that a user is not trying to register with a username that is already chosen, we would have to introduce another validation test that checks $username against the database, or whatever form of storage being used. This validation test was left out because how communication with a storage device works is completely dependent on what that device is and on the setup. Be aware that this would be an important thing to include if you are designing a user registration form, however.
Using this same framework, we can write validation and sanitization for all of our input data.
$username = '';
$email = '';
$realName = '';
$password = '';
$passwordRetype = '';
$errors = array();
// username validation
if (array_key_exists('username', $_POST) && is_string($_POST['username'])) {
$username = trim($_POST['username']);
if ($username == '') {
$errors['username'] = 'You must supply a username.';
} elseif (strlen($username) < 5 || strlen($username) > 12) {
$errors['username'] = 'Your username must be between 5 and 12 characters.';
} elseif (!ctype_alnum($username)) {
$errors['username'] = 'Your username can only contain alphanumeric characters.';
}
} else {
$errors['username'] = 'Username not submitted or not submitted in an expected format.';
}
// email validation
if (array_key_exists('email', $_POST) && is_string($_POST['username'])) {
$email = trim($_POST['email']);
$emailRegexp = '/^[-_a-z0-9\'+*$^&%=~!?{}]++(?:\.[-_a-z0-9\'+*$^&%=~!?{}]+)*+@(?:(?![-.])[-a-z0-9.]+(?<![-.])\.[a-z]{2,6}|\d{1,3}(?:\.\d{1,3}){3})(?::\d++)?$/iD';
if ($email == '') {
$errors['email'] = 'You must supply an email address.';
} elseif (!preg_match($emailRegexp, $email)) {
$errors['email'] = 'Your email address does not appear to be valid.';
}
} else {
$errors['email'] = 'Email address not submitted or not submitted in an expected format.';
}
// realName validation
if (array_key_exists('real-name', $_POST) && is_string($_POST['real-name'])) {
if ($_POST['real-name'] != '') {
$realName = trim($_POST['real-name']);
if (strlen($realName) > 30) {
$errors['realName'] = 'Please abbreviate your name to 30 characters or less.';
}
}
} else {
$errors['realName'] = 'Your real name was not submitted in an expected format.';
}
// password validation
if (array_key_exists('password', $_POST) && is_string($_POST['password'])) {
$password = $_POST['password'];
if ($password == '') {
$errors['password'] = 'You must supply a password.';
} elseif (strlen($password) < 6 || strlen($password) > 20) {
$errors['password'] = 'Your password must be between 6 and 20 characters.';
}
} else {
$errors['password'] = 'Your password was not submitted or not submitted in an expected format.';
}
// passwordRetype validation
if (array_key_exists('password-retype', $_POST) && is_string($_POST['password-retype'])) {
if (!array_key_exists('password', $errors)) {
$passwordRetype = $_POST['password-retype'];
if ($passwordRetype != $password) {
$errors['passwordRetype'] = 'Your password retype did not match.';
}
}
} else {
$errors['passwordRetype'] = 'Your password retype was not submitted or not submitted in an expected format.';
}
The email validation works just like the username validation does. The only unique thing is that it verifies that the email is formed correctly by using a rather long regular expression.
The realName validation looks a bit different. In our requirements, arbitrarily defined, the user's real name is only an optional field. They can choose to fill it out or leave it blank. Therefore, if they did not fill it out, leaving the input as a blank string, we just want to do nothing. However, if they did fill it in, we make sure what they did give us is acceptable and report on any errors otherwise.
The password validation looks typical except for a key thing. We do not trim() the input data. This is because the user may have put whitespace characters on the beginning or end of their password deliberately. It is true that the user could have accidentally included extra whitespace and therefore removing it for them would be in their favour. However, a user is typically aware that they must pay attention and be specific when typing in a password and would usually know when they did or did not include whitespace characters. Removing this whitespace, more often than not, would result in a user who could not login because they were not aware that their leading and trailing whitespace had been removed. Therefore, we take the password as-is.
Validating passwordRetype woks a bit differently. It is tied to whatever $password is. Like the input data for $password, we do not trim the input data for $passwordRetype either. The first test we do verifies that there were no validation errors in $password. If there were, telling the user if their retyped password matched or not would be meaningless. If $password is error-free, we ensure that both $password and $passwordRetype are equal. If they are not equal, we log an error.
Step Five - Handling the Bad Data
All of our data has been validated so now it is time to deal with the results. If there were any errors, we will want to display them beside their respective form fields. We will also want to re-populate the form with the POST data that was given in the last submission. If we do not re-populate the form, all of the input will be lost and the user would have to start from scratch. This would be very annoying if there was only one field with a validation error causing everything to be lost.
<?php if (!empty($errors)): ?>
<p class="error">There are errors in your form submission. Please correct them and try again.</p>
<?php endif; ?>
<form action="form.php" method="post" id="registration-form">
<ol>
<?php if (array_key_exists('username', $errors)): ?>
<li class="error">
<span class="error"><?php echo $errors['username']; ?></span>
<?php else: ?>
<li>
<?php endif; ?>
<label for="registration-form-username">Username:</label>
<input type="text" name="username" id="registration-form-username"
value="<?php echo htmlspecialchars($username); ?>" />
</li>
<?php if (array_key_exists('email', $errors)): ?>
<li class="error">
<span class="error"><?php echo $errors['email']; ?></span>
<?php else: ?>
<li>
<?php endif; ?>
<label for="registration-form-email">Email:</label>
<input type="text" name="email" id="registration-form-email"
value="<?php echo htmlspecialchars($email); ?>" />
</li>
<?php if (array_key_exists('realName', $errors)): ?>
<li class="error">
<span class="error"><?php echo $errors['realName']; ?></span>
<?php else: ?>
<li>
<?php endif; ?>
<label for="registration-real-name">Real Name:</label>
<input type="text" name="real-name" id="registration-real-name"
value="<?php echo htmlspecialchars($realName); ?>" />
</li>
<?php if (array_key_exists('password', $errors)): ?>
<li class="error">
<span class="error"><?php echo $errors['password']; ?></span>
<?php elseif (!empty($errors)): ?>
<li class="error">
<span class="error">Please re-type your password.</span>
<?php else: ?>
<li>
<?php endif; ?>
<label for="registration-form-password">Password:</label>
<input type="text" name="password" id="registration-form-password" />
</li>
<?php if (array_key_exists('passwordRetype', $errors)): ?>
<li class="error">
<span class="error"><?php echo $errors['passwordRetype']; ?></span>
<?php else: ?>
<li>
<?php endif; ?>
<label for="registration-form-password-retype">Retype Password:</label>
<input type="text" name="password-retype" id="registration-form-password-retype" />
</li>
<li>
<input type="submit" name="registration-form-submit" id="registration-form-submit" value="Register" />
</li>
</ol>
</form>
We use both the power of PHP's embedded nature and its templating syntax to deliver our new web form. Now the form can respond to what happened during validation. Before the form, we do a check to see if there are any errors and, if there are, we output a paragraph of text notifying the user. Notice how we include error classes on the parts of the forms containing error messages. This will allow you to easily style the errors to be more obvious using CSS.
For each form field, we see if an error has occurred for it. If one has, we output the error message in a span tag and give both the li and the span the error class. Otherwise, we just output an ordinary li.
For all the input fields except for password and passwordRetype, we echo the values contained in each of their respective variables into their value attributes after escaping HTML characters with htmlspecialchars(). Remember how we initialized all of the field value variables to blank strings? This is because, if we did not, they would not be set unless POST data was sent because inside the validation blocks are the only other place we give values to them. POST data will not be sent when a user first visits the form and the POST data we expect will not be sent if the user tries something malicious. Using a variable that does not exist generates a notice-level error just like trying to lookup a value in an array with a key that does not exist, so we avoid the chance of it happening altogether.
We run the data of each variable through htmlspecialchars() before echoing it into the value attribute. This is an important escaping step. If the user intentionally, or unintentionally, included HTML characters such as a double quote ("), it could have potentially broken the HTML code. If you do not see how this could happen, try echoing the variable without using htmlspecialchars() and submit any data containing a single double quote. Look at the result of the page and look at the source code through the browser. You will see what has happened. Be aware of the context you are putting your data in and make sure that the data will not be interpreted unexpectedly.
Something special that we do for the password field is that, because its data is not kept when there were errors in the form, we want to ensure the user knows to re-type their password. If there were no validation errors with the password, the user would not see any message about it at all. Therefore, we detect the case where there is no password error but there are other errors and output a custom message for the situation.
Step Five - Pulling Things Together & Handling Good Data
Now we have validated our data and written a form that will respond to the validation results by printing our error messages. It will also re-populate form data sent in a previous request to save the user from having to start over. What we have not yet done is placed the parts together to get an overall working script. Right now we have a bit of code that does validation and a bit of code that displays a form. We need to tie these things together.
When our script runs, it should be able to do three main things. If the script is run without any POST data present at all, it means the user has just visited the form for the first time. In that case, we should not be running any validation at all. The second thing our script needs to be able to do is validate the input when there is POST data present and re-display the form with errors that occur.
The final thing our script needs to do is, if there are not any errors in the POST data, do something with the input data and display a success message. In our case, what we want to do with good POST data is save a new user to our storage device, perhaps a database. How to best show a success message can vary from one situation to another. Decide on something that is the most intuitive and convenient for the user.
A special note: a browser will remember the POST data a user submitted on the last page. To clear this data, the easiest thing to do is redirect the browser with the header() function after you have done something with the data. If the browser is redirected, and thus asked to load a new page, it will lose memory of the POST data previously submitted. Without making sure the browser has cleared its memory of the POST data, it will warn the user that the data will be resubmitted when they try to reload the page. This is more often than not undesirable for the user, be it that they waste time trying to get around the POST data resubmission or they continue on anyways and suffer any results from having the same data submitted twice. This could cause serious side effects or just an "oops, double post!" depending on what your script does. Make sure to assess this matter when working with POST data.
The method that we will use to display our success message for our script is replacing the form with the message without doing any redirect at all. This is done just for compactness as we can fit this into one file and keep it independent of other systems. A better approach may be redirecting them to their new user panel, or to the login screen, or similar. Submitting the data a second time should not cause damage anyhow, so long as you validate two of the same user cannot exist.
Our overall structure will therefore look like the following.
<?php
$username = '';
$email = '';
$realName = '';
$password = '';
$passwordRetype = '';
$errors = array();
if (!empty($_POST)) {
// validate the POST data
if (empty($errors)) {
// save the user
}
}
?>
<?php if (!empty($_POST) && empty($errors)): ?>
<span class="success">Your new user account has been successfully registered.</span>
<?php else: ?>
<!-- display the form -->
<?php endif; ?>
If we replace the validation comment with the validation code and the form display comment with the form code, we should be good to go. The introductions here are minor. !empty($_POST) will be true if there was any POST data at all submitted. If there was, we validate that POST data. If there are no errors, verified by the empty($errors) test, we save the new user to storage.
In the presentation logic we first do a check to see if there is POST data and there were not any errors. If this is the case, we must have run a successful validation and the data was good to use. In a real application, you would want to flag the success in a different manor to allow the ability to report on problems with connecting to the database, communicating with a mail server, or similar. Errors can still occur and still need to be dealt with when doing something with the data. You should never be telling the user their action was successful when it was, in fact, not! If all is well, though, we redisplay the form. If POST data was submitted, the validation steps will have populated the field value variables and the form will be re-populated. Otherwise we will just see a blank form ready to be filled out.
Give this code a try to see the results for yourself.
To demonstrate common escaping practices, let us pretend you are using a MySQL database to store the users and you are fond of the mysql_* functions. Hint: use PDO instead if you are actually interacting with a database. To make the data safe to use to build our query, we need to do an additional escaping step.
$username = mysql_real_escape_string($username);
$realName = mysql_real_escape_string($realName);
// and so on...
As you should know, before building a query string to execute with mysql_query(), we need to escape the unknown data that we are putting into it. MySQL provides a function to do this called mysql_real_escape_string(). It is imperative that you use it on any data going into a query string being sent to a MySQL database. Without doing this, you open the door to SQL injection attacks which can be nasty.
Be very attentive to what kind of data is allowed through our sanitization, validation, and escaping. One of our input fields can potentially contain dangerous data. The username field is restricted to alphanumeric characters, so not much will come of it. The email has to conform to an email format, so not much can come of it either. The passwords will likely be hashed and anything harmful will be lost. Pay close attention to the validation of realName, however.
The only restraints on the realName variable is that it must be a string under 30 characters. What is in that string we know nothing of and it could in fact not be someone's name. Say that on our make-believe website we publicly display people's real names. If someone's name was in fact HTML code, it would be rendered by an innocent user's browser and potentially result in a cross-site scripting attack. Thirty characters is not much to work with but it could potentially be enough so do not skimp. We should either sanitize the string using strip_tags() before saving it to the database in addition to mysql_real_escape_string(), or use strip_tags() on the string before displaying it to a web browser. If we, in fact, wanted to allow HTML characters in someone's name but did not want them treated as HTML code, we can escape the data with htmlspecialchars(), like we used earlier.
The purpose of sanitization was to ensure our validation tests were clear and made sense to the user by stripping out extra whitespace. The purpose of our escaping is to ensure our data is safe for usage in our task, or action, or whatever you want to call the thing you do with your data. Be certain to assess what sanitization steps you will need to take with your input data based on what you are going to be doing with it. For example, if you want to send an email, be sure to guard against email injection.
Conclusions
The framework that I provide for processing web forms is only a suggestion. Feel free to experiment with your own structures as there are nearly unlimited ways to conquer the task. Some of you may be familiar with web frameworks and many of the offer quite abstracted ways to deal with input. Just remember that they are following the same principles that we are here and to keep those principles in mind.
- PHP parses POST data into the $_POST super global and GET data into the $_GET super global.
- What method a form uses depends on its method attribute. Set it appropriately according to your needs.
- Remember that GET data is transmitted through the query string in the URI so that it can be bookmarked. GET data is limited in the amount of information it can hold because of URI length limitations. Use these requests for actions that do not change state of your application.
- POST data is submitted transparently and cannot be bookmarked. POST data can be used to submit large amounts of information. Use these requests for actions that change state of your server.
- You cannot lookup a value in an array with a key that does not exist. Verify that it does by using isset(), empty(), array_key_exists(), or another alternative. This is especially important for keys in super globals because there is no guarantee that it will exist.
- A browser will remember the POST data that was submitted on the last page. If a user tries to refresh their browser, it will prompt them to resubmit the POST data. Be aware of the potential dangers of a user accidentally submitting the same POST data twice. Use a redirect to clear the browser's memory of POST data if the consequences of resubmitting are undesirable or destructive.
- Be familiar with SQL injection attacks, cross-site scripting attacks, email injection attacks, and others such as cross-site request forgery. Ensure that you have sufficient knowledge on how to safely use unknown data with your task at hand.
The number one thing to remember: never trust input. This applies to more than to just data in $_GET and $_POST; any input that is coming into your script must be labeled as flawed and dangerous. Consider all the possibilities of what your input could be and how it could be understood incorrectly in its later uses; always make sure to sanitize, validate, and escape your input appropriately. If you cannot accept the data, make sure to clearly inform the user of what issues have occurred.
For better memory, remember save yourself by using the SA(nitize), V(alidate), and E(scape) rule. These practices must become autonomous as they can be easily overlooked.
You can download the full source to the sample at http://www.needtodevelop.com/examples/phpform.zip. Please leave comments to help me clarify and improve this article as much as possible.