PHP Security Mistakes
The purpose of this document is to inform PHP programmers of common security mistakes that can be overlooked in PHP scripts. While many of the following concepts may appear to be common sense, they are unfortunately not always common practice. After applying the following practices to your coding, you will be able to eliminate the vast majority of security holes that plague many scripts. Many of these security holes have been found in widely-used open source and commercial PHP scripts in the past.
The most important concept to learn from this article is that you should never trust the user to input exactly what is expected. The way most PHP scripts are compromised is by entering unexpected data to exploit security holes inadvertantly left in the script.
Always keep the following principles in mind when designing your scripts:
1. Never include, require, or otherwise open a file with a filename based on user input, without thoroughly checking it first.
Take the following example:
Since there is no validation being done on $page, a malicious user could hypothetically call your script like this (assuming register_globals is set to ON):
Therefore causing your script to include the servers /etc/passwd file. When a non PHP file is include()’d or require()’d, it’s displayed as HTML/Text, not parsed as PHP code.
On many PHP installations, the include() and require() functions can include remote files. If the malicious user were to call your script like this:
He would be able to have evilscript.php output any PHP code that he or she wanted your script to execute. Imagine if the user sent code to delete content from your database or even send sensitive information directly to the browser.
Solution: validate the input. One method of validation would be to create a list of acceptable pages. If the input did not match any of those pages, an error could be displayed.
$pages = array(‘index.html’, ‘page2.html’, ‘page3.html’);
if( in_array($page, $pages) )
2. Be careful with eval()
Placing user-inputted values into the eval() function can be extremely dangerous. You essentially give the malicious user the ability to execute any command he or she wishes! You may envision the input coming from a drop-down menu of options you specify, but you user may decide to send input like this:
By putting his own code in that statement, the user could cause your program to output your server’s complete /etc/passwd file.
Use eval() sparingly, and by all means, validate the input. It should only be used when absolutely necessary — when there is dynamically generated PHP code. If you are using it to substitute template variables into a string or substitute user-inputted values, then you are using it for the wrong reason. Try sprintf() or a template system instead.
3. Be careful when using register_globals = ON
This has been a major issue since this feature was invented. It was originally designed to make programming in PHP easier (and that it did), but misuse of it often led to security holes. As of PHP 4.2.0, register_globals is set to OFF by default. It is recommended that you use the superglobals to deal with input ($_GET, $_POST, $_COOKIE, $_SESSION, etc).
For example, let us say that you had a variable that specified what page to include:
but you intended $page to be defined in a config file or somewhere else in the script, and not to come as user input. In one instance you forgot to pre-define $page. If register_globals is set to ON, the malicious user can take over and define $page for you, by calling your script like this:
I recommend you develop with register_globals set to OFF, and use the superglobals when accessing user input. In addition, you should always develop with full error reporting, which can be specified like this (at the top of your script):
This way, you will receive a notice for every variable you try to call that was not previously defined. Yes, PHP does not require you to define variables so there may be notices that you can ignore, but this will help you to catch undefined variables that you did expect to come from input or other sources. In the previous example, when $page was referenced in the include() statement, PHP would issue a notice that $page was not defined.
Whether or not you want to use register_globals is up to you, but make sure you are aware of the advantages and disadvantages of it and how to remedy the possible security holes.
4. Never run unescaped queries
PHP has a feature, enabled by default, that automatically escapes (adds a backslash in front of) certain characters that come in from a GET, POST, or COOKIE. The single quote (‘) is one example of a character that is escaped automatically. This is done so that if you include input variables in your SQL queries, it will not treat single quotes as part of the query. Say your user entered $name from a form and you performed this query:
UPDATE users SET Name=’$name’ WHERE ID=1;
Normally, if they had entered $name with single quotes in them, they would be escaped, so MySQL would see this:
UPDATE users SET Name=’Joe\’s’ WHERE ID=1
so that the single quote entered into “Joe’s” would not interfere with the query syntax.
In some situations, you may use stripslashes() on an input variable. If you put the variable into a query, make sure to use addslashes() or mysql_escape_string() to escape the single quotes before your run the query. Imagine if an unslashed query went in, and a malicious user had entered part of a query as their name!
UPDATE users SET Name=’Joe’,Admin=’1′ WHERE ID=1
On the input form, the user would have entered:
As their name, and since the single quotes were not escaped, he or she would be able to actually end the name definition, place in a comma, and set another variable called Admin!
The final query with input in blue would look like this:
UPDATE users SET Name=’Joe’,Admin=’1′ WHERE ID=1
In some configurations, magic_quotes_gpc (the feature that automatically adds slashes to all input) is actually set to OFF. You can use the function get_magic_quotes_gpc() to see if it’s on or not (it returns true or false). If it returns false, simply use addslashes() to add slashes to all of the input (it is easiest if you use $_POST, $_GET, and $_COOKIE or $HTTP_POST_VARS, $HTTP_GET_VARS, and $HTTP_COOKIE_VARS, instead of globals because you could step through those arrays using a foreach() loop and add slashes to each one).
5. For protected areas, use sessions or validate the login every time.
There are some cases where programmers will only use some sort of login.php script to first validate their username and password (entered through a form), test if they’re an administrative or valid user, and actually set a variable through a cookie, or even hide it as a hidden variable. Then in the code, they check to see if they have access like this:
// let them in
// kick them out
The above a code makes the fatal assumption that the $admin variable can only come from a cookie or input form that the malicious user has no control over. However, that is simply not the case. With register_globals enabled, injecting designed input into the $admin variable is as easy as calling the script like so:
Furthermore, even if you use the superglobals $_COOKIE or $_POST, a malicious user can easily forge a cookie or create his own HTML form to post any information to your script.
There are two good solutions to this problem. One is on the same track as setting an $admin variable, but this time set $admin as a session variable. In this case, it is stored on the server and is much less likely to be forged. On subsequent calls to the same script, your user’s previous session information will be available on the server, and you will be able to verify if the user is an administrator like so:
if( $_SESSION[‘admin’] )
The second solution is to only store their username and password in a cookie, and with every call to the script, validate the username and password and verify if the user is an administrator. You could have two functions — one called validate_login($username,$password) that verified the user’s login information, and one called is_admin($username) that queried the database to see if that username is an administrator. The code would be placed at the top of any protected script:
if( !validate_login( $_COOKIE[‘username’], $_COOKIE[‘password’] ) )
echo “Sorry, invalid login”;
// the login is ok if we made it down here
if( !is_admin( $_COOKIE[‘username’] ) )
echo “Sorry, you do not have access to this section”;
Personally I recommend using sessions, as the latter solution is not scalable.
6. If you don’t want the file contents to be seen, give the file a .php extension.
It was common practice for awhile to name include files or library files with a .inc extension. Here’s the problem: if a malicious user simply enter the .inc file into his browser, it will be displayed as plain text, not parsed as PHP. Even if the browser did not like the file type, an option to download it would most likely be given. Imagine if this file had your database login and password, or even more sensitive information.
This goes for any other extension other than .php (and a few others), so even a .conf or a .cfg file would not be safe.
The solution is to put a .php extension on the end of it. Since your include files or config files usually just define variables and/or functions and not really output anything, if your user were to load this, for example, into their browser:
they would most likely be shown nothing at all, unless of your lib.inc.php outputs something. Either way, the file would be parsed as PHP instead of just displaying your code.
There are also some reports of people adding Apache directives that will deny access to .inc files; however, I do not recommend this because of the lack of portability. If you rely on .inc files and that Apache directive to deny access to them and one day you move your scripts to another server and forget to place the Apache directive in, you are wide open.