I have here a list of common vulnerabilities found in PHP websites divided up into five different categories and some suggestions about how to fortify them. I have seen many of these in live, professional sites. Following these suggestions should help to keep your websites more secure, but note that this is not an extensive, all-encompassing list. There are many other vulnerabilities out there.

Validation

Overview: User-supplied data should be validated to ensure that it matches what is expected/proper.

Main Points:

Whitelists: Arguments to functions such as include(), include_once(), require(), and require_once() should be whitelisted when forming them dynamically. In fact, whitelists should always be preferred over blacklists whenever possible.

//Here is bad code:
include($_GET['page'] . ".php"); 
//not good if you were expecting "ideas" and I gave "admin/members"
//Here is better code:
$page = "";
switch($_GET['page']){
    case "ideas": $page="ideas.php"; break;
    case "members": $page="members.php"; break;
    default: $page="start.php";
}
include($page);

Cross Site Request Forgery (CSRF): This type of vulnerability involves a malicious user sending commands to the server using the privileges of another unsuspecting user. This is possible when sites don't validate POST and GET data sent to them to ensure that it came from the proper place.

For example: Let's say we have a URL which will delete a user with the id of 5: website.com/deleteUser.php?id=5

A malicious user can make a webpage and include <img src='http://website.com/deleteUser.php?id=5'/>. Then the malicious user gets an admin user to visit www.malicousUsersSite.com, and when he loads the page it will send a request to Website's server asking for the page deleteUser.php. Malicious user's page will see that it is not an image and just reject the rest of the response and display a broken image, but Website's server will continue to process the request. If the admin user is authenticated with Website, Website will delete the user.

To solve this, a correct request should look like: website.com/deleteUser.php?id=5&token=23sd-32oj-32kjo-2oi34
The token should be different for each session (or even better it should be different for every page as well as each session, but this is more complicated). When malicious user adds his token to the end of the URL and he gets admin user to view his page, Website will receive a request, then it will check if the token it received matches the token it expects for admin user and it will find that it does not match and then it will reject the request.

For more detailed information about CSRF, see Cross-Site Request Forgery - Demystified.

Validate Emails: Ensure emails are in proper email format. To check this easily and cleanly, use:

filter_var($email, FILTER_VALIDATE_EMAIL);

Don't assume that just because you used type='email' on your input or because you validated it with javascript that you have a valid email on the server-side.

Sanitize User Input

Overview: User supplied data should be sanitized before it touches the database or gets printed to the HTML.

Main Points:

If You Didn't Write It; Sanitize It: Sanitize anything printed to the screen which you didn't write explicitly in the code. Things such as $_SERVER['X_HTTP_FORWARDED_FOR'] and $_SERVER['USER_AGENT'] may seem safe, but they can be altered by the user.

Database: Send database queries through a database class which sanitizes the user-input, because if you manually sanitize inputs each time, you're bound to forget once.

//example
$result = mysql_query("select 1 from users where username='{$_POST['username']}' and password='{$_POST['password']}'");
//You may have been expecting username:password, but 
//what you might get is username:' or 1=1 --

The best way to prevent SQL injections is to update your MySQL extension to the newer MySQL extension, PDO_MySQL and MySQLi, and use prepared statements (with any user-supplied data passed in as a parameter to the prepared statement). It is strongly recommended to update your extension because the basic MySQL extension has been deprecated (and in PHP 7 it is no longer supported). If for some reason you have no choice in the API you use, sanitize user input with mysql_real_escape_string() as a minimum. Do not use addslashes(); this is not a safe function.

Also, database users should only have the minimum level of permissions necessary. For instance, if the code only ever selects/updates/deletes/inserts data, that's all the user should be able to do. There's no reason for the user to have create/drop privileges when it isn't supposed to ever perform those actions.

XSS Prevention: Sanitize any user-input or database input being printed to the HTML.

Don't use strip_tags() to sanitize data being used as an attribute of an HTML tag; use htmlentities($variable, ENT_QUOTES) instead (the ENT_QUOTES flag tells htmlentities to santize both single and double quotes).

echo "<input type='text' name='title' value='" . strip_tags($_POST['title']) . "'/>";
//This might prevent you from inputting something like this: 
//'/><script>alert(1)</script>
//But you can still do this: ' onmouseover='alert(1)

Direct Code Injection: Avoid using eval() or system() or popen() on user supplied data. This can result in terrible things. If you do need to use one for something, never put any user-supplied data inside it.

eval("\$x = {$_GET['val']};"); //we expected something like "3"
//But we received something like: "3; unlink('index.php')"


Directory Traversal: Sanitize any user input being sent to functions like fopen() and unlink() to prevent directory traversals.

@unlink(PATH_TO_UPLOAD.$_GET['delete']);
//we expected something like 'oldImage.jpg'
//however, what we receive could be '../../../index.php'


Send Mail: Sanitize data being sent with the mail() function to prevent CRLF (carriage return; line feed) injections.

//Let's say this is a contact form
mail($to, $subject, $message, $headers);
//We expect $to to be something like: guysEmail@yahoo.com 
//What a hacker could make it is: 
//guysEmail@yahoo.com%0d%0aBCC:someOtherGuy@yahoo.com 
//Now we'll BCC some other guy too. Other email headers 
//can be added and changed as well.


PHP_SELF: Never use $_SERVER['PHP_SELF'] in the action field of a form. There's no reason to since action='' is the same thing and it's safer and easier to type. If you need to know the path to the currently executing script, use $_SERVER['SCRIPT_FILENAME'] instead and just cut out the path to the webroot (i.e. $_SERVER['DOCUMENT_ROOT']).

echo "<form action='{$_SERVER['PHP_SELF']}' method='post'>";
//if this form appears on submit.php, I can visit:
//submit.php/"><script>alert(1)</script> 
//and this would appear as $_SERVER['PHP_SELF']

Login Management

Overview: Passwords should be generated with a strong random function, stored with a strong hashing algorithm, and login and sessions should be maintained safely.

Main Points:

Random-Generated Passwords: Use a large keyspace for randomly generated passwords (e.g. [a-zA-Z0-9]) and make passwords at least 10 characters to increase the necessary brute-forcing time.

Failed Logins: After some number of failed logins within a certain period of time (e.g. five failed attempts in one hour), restrict login to prevent brute forcing. Ways to restrict login can differ, it could be per session, but it's easy to generate a new session. It could be for the username, but that could be used to lock a targeted user out. It could be per IP, but this doesn't work when a large group shares one IP.

Secure Hashes: Ideally, passwords should be hashed with an algorithm such as bcrypt which was intended for password hashing. Many other hashing algorithms such as MD5 were designed for message digests, meaning that they should process large amounts of data very quickly. This speed unfortunately makes it much easier to brute force. If you aren't able to use bcrypt, as a minimum you should prefer SHA256 over MD5, and salts should be used.

hash('sha256', $salt.$password);

Session Fixation: On login, invalidate the current session to prevent session fixation.

session_regenerate_id();

Session fixation occurs when a malicious user visits a site, then, through some means, gives his session to another user (e.g. adminUser). MaliciousUser then waits for adminUser to login, and then MaliciousUser can refresh his page and will be logged in as adminUser because his session has been authenticated.

Session Stealing: Link session to IP to reduce the chances of session stealing and session fixation being effective.

Admin Access: Admin access should not be solely IP-based. This is vulnerable to IP-Spoofing. However, adding IP restrictions in addition to password authentication adds security.

Erroring Securely

Overview: Errors should be clean and should not reveal any sensitive information. (Note: "erroring" is not a real word, but here it means "to throw an error")

Main Points:

Custom 404 Page: Access to restricted pages should redirect to a 404 page to prevent users from being able to see the existence of restricted pages.

Clean Error Messages: On errors, don't display any technical information to the users; that information should be in logs. Error should also not dump the user input because this can cause XSS injections.

It's generally better to turn off display_errors in your php.ini file to prevent things like full path disclosure.

Protect User Information: On failed login and on lost password reset, don't report the cause. This allows hackers to infer information about the users. Failed logins should also be logged.

Miscellaneous

Overview: These are some other things that don't really fit into another category. Some may seem obscure, but they do occur in the wild.

Main Points:

Keep Updated: Always use the latest stable version of PHP and your webserver. (for example, old versions of PHP have a lot of vulnerabilities such as http header splitting which have been addressed in recent versions).

Register Globals: Always ensure that the php configuration setting register_globals is off. This option allows indices from $_REQUEST to be automatically made available as a PHP variable, e.g. $_POST['test'] would be available as $test.

if(get_user_access()=='admin'){
  $hasAdminAccess = true;
}
if ($hasAdminAccess) { ... }
//If get_user_access() returned something like 'member' it wouldn't have entered the body of the if statement, 
//but you had the GET parameter page.php?hasAdminAccess=true, it would set $hasAdminAccess=true

Flood Protection: Flood protection should be implemented to protect against spamming and brute forcing. This can be implemented, for example, by forcing users to wait some seconds in-between posting content.