Skip navigation

PHP is a powerful and easy language to learn. As a developer, you must use that power in a safe and effective manner. While being easy to learn means that it is easy to pick up, it also means that you may not have any experience with writing secure code. Writing secure code is a must, however, if you do not want your server to be compromised, user data stolen, your site destroyed, or quite possibly worse. In this article, you will learn about writing more secure PHP code.

Trusting the User

First of all, just don’t do it. The number one, most important rule you can have as a developer is to never, ever, trust the data you receive from your users. Ever. A large number of the vulnerabilities (some of which I’ll be discussing) can actually be avoiding if the proper precautions are taken and the user’s data is properly processed. As a developer you might think “none of the people that are interested in my web site would be malicious, so I don’t have to worry about that.” That isn’t a wise idea to have, however, as a user could unintentionally enter some data in that exploits a security hole, and now you are up a creek without a paddle.

Let me reiterate this point. Never, ever trust data from the user. Even if you check something client side with JavaScript, you should not trust it. Remember that some people turn off JavaScript, making those checks completely useless. Being paranoid about user input and assuming everything they can do could be malicious will greatly prevent many of the common security vulnerabilities found in PHP applications.

Descriptive Error Messages

While these can be a big help when debugging, descriptive error messages can also be a big help to potential attackers. Based on the information given in an error message, a malicious user could find out information about your application’s database structure, file system layout, or other worse things. Make sure that any error checking you do with the users data will give the least amount of information needed by the user to correct whatever they did wrong.

In addition to your own generated errors, PHP can report a variety of script errors, warnings, and notices, all of which a malicious user could use against you. The good news is that you can disable these on your production server with an .htaccess directive or in your php.ini file by setting error_reporting to 0. Do not try using ini_set though! If a fatal error occurs in your script, the value you specify with ini_set will not have any affect, so everything you are trying to prevent being displayed will still be displayed.

The best way to keep track of the errors, warnings, and notices that are not important to the user, but are useful to you when debugging is to use PHP’s built in logging functions. You can do this just like you do with error reporting by setting the error_logging directive to true and setting the error_log directive to the filename that you want to log to. If these are set with ini_set, the error simply won’t be logged so it isn’t a security risk (just harder for you as the developer to debug). Once you have the log file setup, simply use the trigger_error function like so:

trigger_error('A serious error occurred: DETAILS', E_USER_ERROR);

The first parameter is a string with whatever you want to be logged. The second parameter is the PHP error type, which defaults to E_USER_NOTICE if left unspecified.

Insecure Database Setup

The default username and password for popular databases are well known. Leaving these at their default value allows for easy entry to anyone who is able to figure out where your database server is. The default setup is the first thing an attacker would try in an attempt to get in, so try not to make their job easier!

It is also a good idea to set up different users with specific privileges for your web application. Ideally, anything that a normal user can do should fall under a limited account. By only allowing the client’s database user to insert and update tuples, or rows in the table, you drastically cut back on the type of damage they can do. Of course, you’ll probably need to have some ability to delete an arbitrary tuple, but with this restriction it seems impossible to do. The good news is that there is a trivial way to get around this. Simply add another field to the database called something like is_deleted and have it set to 0 by default. When you want to delete something, simply set it to 1. When you are querying data, just look for results that do not have is_deleted set to 1, and it will appear to be deleted. The administrators should use a different database user that allows for deleting, and every so often they can run a script that deletes these entries.

SQL Injection Attacks

SQL injection attacks come about from developers trusting their users to not be malicious. These attacks are the most common security hazard when communicating with a database. Since the most popular database system in use today with PHP applications is MySQL, these examples are going to be written with its api functions. Let’s take a look at the common example of a login script. Generally, they’ll be some query like this:

$sql = “SELECT * FROM `user` WHERE `uname` = '{$_POST['uname']}' AND `pwd` = '{$_POST['pwd']}'”;
$query = mysql_query($sql);

While this might look safe, it isn’t. What if some user entered in “' OR 1 = 1 #” as their username? Now the query will result in all the rows in the table being returned, but that’s clearly not what we want! The query that is executed will look like this:

SELECT * FROM `user` WHERE `uname` = '' OR 1 = 1 # AND `pwd` = ''

This clearly isn’t what we wanted, so let’s take a look why this is the result. The hash symbol (#) indicates a comment, and MySQL will simply ignore everything after it. Since one always equals one, all tuples will be returned. Now, if the developer didn’t check for the number of rows and just grabs the first result returned by the query, the attacker will now be logged in as the first user in the user table. You’ll often find that the first user is an administrator of some type, so this attacker can now go around your site as an administrator. With a bit more effort, the attacker could create an account for themselves, modify entries in the table, or worse. This all happened because we violated our first rule and trusted user input.

There are two solutions to this problem, and both of which are fairly easy to implement. If you are determined to use the mysql_* functions (or are required to use PHP < 5.1), you have to escape the users input like so:

$uname = mysql_real_escape_string($_POST[‘uname’]);
$pwd = mysql_real_escape_string($_POST[‘pwd’]);
$sql = “SELECT * FROM `user` WHERE `uname` = ‘$uname’ AND `pwd` = ‘$pwd’”;
$query = mysql_query($sql);

Be careful if the magic_quotes_gpc directive is set, as you’ll have to remove the slashes it adds before calling mysql_real_escape_string because some things will get escaped twice. The magic_quotes_gpc directive adds a slash before single-quotes, double-quotes, backslashes, and NULs. MySQL’s function escapes three additional characters; \n, \r, and \x1a. As a result, some things are escaped twice, which produces results other than what we intended.

If you are lucky enough to be using PHP 5.1 or later, you’ll have the ability to use the PHP Data Object (PDO) class. The PDO class is an abstraction layer used to interface with databases. The best part about it is that it was designed in a way that defending against SQL injection attacks is just part of the process. Let’s take a look at how we can write our query with PDO:

$stmt = $db->prepare(“SELECT * FROM user WHERE uname = :uname AND pwd
= :pwd”);
$stmt->execute(array(':uname' => $_POST['uname'], ':pwd' => $_POST['pwd']));
$result = $stmt->fetch();

Calling PDO->prepare and then PDOStatement->execute gives you two benefits. First, it will properly escape the variables you pass in, preventing the SQL injection attack. Secondly, if you plan on issuing a SQL statement multiple times, calling PDO->prepare initially, then calling PDOStatement->execute each time you want to run the query, your calls will be optimized by the database driver by caching the query plan and meta data. This can result in huge performance benefits. There is also a function that is just like mysql_query, and that’s PDO->query, but I wouldn’t recommend using it because of the benefits obtained by PDO->prepare and PDOStatement->execute.

Improper Handling of Includes

Some websites like to dynamically include another page with a url parameter. While this can be incredibly useful, it can also be incredibly dangerous. The PHP code to include this file might look something like this:

Now, a url like index.php?p=news.html. works just fine. However, we are trusting our user here to not enter something malicious in the query string. What if the url is index.php?p=.htpasswd? The malicious user suddenly has our .htpasswd file, and with the right resources, could figure out passwords to log in to secured parts of the website. The attacker could potentially get any file that your webserver has permission to access. The fix for this is to check the filename against a list of ones that you are expecting. You should also set the PHP directive open_basedir to the paths that you will be opening files from. PHP will then make sure that you never open a file outside of the specified path(s).

If your server is not configured properly, this hole could be used to execute a remote php script as well, letting the attacker run arbitrary code on your server. To prevent this, set the allow_url_fopen directive to false. This can only be set in your .htaccess file or php.ini, however, so don’t try using ini_set.

Conclusion

Assuming that your site is safe from attackers without actually taking steps to ensure that it is safe is an accident waiting to happen. Assuming that everyone is out to get you and your site, while it might seem to be a symptom of paranoia, can save you a lot of headaches if something bad were to happen. Most of the common security problems with PHP are the result of trusting the user, so remember our first rule, never, ever trust the data you receive from your user.