PHP Security

Posted on 2008-11-28. Last updated on 2015-04-05.
PHP web applications are one of the most commonly attacked pieces of software on the Internet today. Anyone who has looked at their web server logs can attest to the frequency of probes for vulnerable PHP applications. PHP's easy learning curve has lead to its popularity and breadth of applications, but not without some hard learned lessons on the way. This document serves as a reminder of some of the important security related issues when programming in PHP. The paper is not a security manual. The paper is just a collection of notes. If you are writing PHP applications I strongly suggest that you research the subject in depth.

Always write your applications with security in mind!

Table of Contents

  1. Introduction
  2. Golden rules
  3. Links

Introduction

Thinking about security isn't something we tend to do when we write code, but it is something that we must get into the habit of thinking about from the very moment we start coding.

When developing websites using PHP you have to think about security all the time. A poorly written PHP application can serve as an open door into the underlying database, if a database is used, or it can even serve as an open door into the operating system itself.

PHP is a weakly typed programming language and it wasn't developed with security in mind. A lot of things can go wrong when you program in PHP and there exists a lot of functionality that is best to avoid altogether.

When working with PHP, or any other programming language, thinking about security can become a habit if you follow some simple guidelines.

Golden rules

Avoid $_REQUEST

Avoid the usage of the associative array $_REQUEST.

The variables in $_REQUEST are provided to the script via the GET, POST, and COOKIE input mechanisms and therefore could be modified by the remote user and cannot be trusted. The presence and order of variables listed in this array is defined according to the PHP variables_order configuration directive.

The reason that $_REQUEST is problematic is that it takes values from both $_GET, $_POST, and $_COOKIE. This means that you not only have to filter and validate the data, but you also need to keep track of where the data is coming from.

It is better to use $_GET, $_POST, and $_COOKIE directly rather than to use $_REQUEST.

In any case you are still required to filter and validate user input meticulous.

Never ever trust user input

User input must always be filtered and validated. You have to check for type and valid character set.

In order to prevent SQL Injection data that goes into a database must be protected using prepared statements. Prepared statements uses placeholders for the input data making it impossible for the database to interpret any of the values. This completely mitigates SQL injection.

It's worth to mention that data that goes into the database shouldn't be changed!

Don't create or use functions that add slashes or other escape characters to the data in such a way that you have to remove those from the data when it is fetched from the database. The process of filtering and validating data must also preserve the data so it should never be necessary to reverse anything. Functions such as stripslashes() demonstrates a really bad design.

Using prepared statements with PHP PDO preserves the data completely so you don't need to use something like stripslashes().

Filtering user input also protects against a lot of Cross-Site Scripting techniques.

Also don't trust addslashes(). And don't trust PHP's $_SERVER variable. The name of this variable is deceiving because one tends to think that all data comes from the server. While that is true per say, a lot of it comes from the browser or user agent and it is then feeded to the server which then feeds it back to the PHP script.

The output of $_SERVER['HTTP_HOST'], as such an example, is from the user agent, not from the server.

Always escape output

Escaping output means that you transform data, even when it is coming from yourself, to a format suitable for the output medium.

Any display output from a database to the browser should always have htmlspecialchars called on it.

Example:

$my_evil_string = "<script>alert('XSS');</script>"
echo htmlspecialchars($my_evil_string, ENT_QUOTES, "UTF-8");

If the string in the above example wasn't escaped using htmlspecialchars() it would activate the Javascript (if Javascript was enabled in the browser).

If the output is going to be GET parameters in an URL you should always call urlencode() on it first, then followed by htmlentities().

Example:

$query_string = 'foo=' . urlencode($foo) . '&bar=' . urlencode($bar);
echo '<a href="mycgi?' . htmlspecialchars($query_string) . '">';

Escaping output protects against malicious Javascript that can be used in a number of Cross-site Scripting attacks.

However, such malicious content must first be stored before it can be used, as such filtering and validating input goes hand-in-hand with escaping output.

Be careful when including files

Never load or read files into your application if the file itself is depending upon user input.

Always use full paths when including files.

If you are dynamically including files make sure you white list them and use a filter to make sure that the files originates from the right place and has the allowed content. Don't trust the browser to provide the correct mime type and don't just use the file extension to determine the correct mime type. Any kind of file can have a false file extension.

Example of a mime type detection function (for Linux and BSD only):

function getMimeType($filename)
{
    if(!function_exists('mime_content_type')) {

        function mime_content_type($filename) {

            $mime_types = array(

                'txt'   => 'text/plain',
                'htm'   => 'text/html',
                'html'  => 'text/html',
                'php'   => 'text/html',
                'css'   => 'text/css',
                'js'    => 'application/javascript',
                'json'  => 'application/json',
                'xml'   => 'application/xml',
                'swf'   => 'application/x-shockwave-flash',
                'pdf'   => 'application/pdf',
                'psd'   => 'image/vnd.adobe.photoshop',
                'ai'    => 'application/postscript',
                'eps'   => 'application/postscript',
                'ps'    => 'application/postscript',
                'doc'   => 'application/msword',
                'rtf'   => 'application/rtf',
                'xls'   => 'application/vnd.ms-excel',
                'ppt'   => 'application/vnd.ms-powerpoint',
                'flv'   => 'video/x-flv',
                'png'   => 'image/png',
                'jpe'   => 'image/jpeg',
                'jpeg'  => 'image/jpeg',
                'jpg'   => 'image/jpeg',
                'gif'   => 'image/gif',
                'bmp'   => 'image/bmp',
                'ico'   => 'image/vnd.microsoft.icon',
                'tiff'  => 'image/tiff',
                'tif'   => 'image/tiff',
                'svg'   => 'image/svg+xml',
                'svgz'  => 'image/svg+xml',
                'zip'   => 'application/zip',
                'rar'   => 'application/x-rar-compressed',
                'exe'   => 'application/x-msdownload',
                'msi'   => 'application/x-msdownload',
                'cab'   => 'application/vnd.ms-cab-compressed',
                'mp3'   => 'audio/mpeg',
                'qt'    => 'video/quicktime',
                'mov'   => 'video/quicktime',
                'odt'   => 'application/vnd.oasis.opendocument.text',
                'ods'   => 'application/vnd.oasis.opendocument.spreadsheet'
            );

            $ext = strtolower(array_pop(explode('.',$filename)));

            if (array_key_exists($ext, $mime_types)) {

                return $mime_types[$ext];

            } elseif (function_exists('finfo_open')) {

                $finfo = finfo_open(FILEINFO_MIME);
                $mimetype = finfo_file($finfo, $filename);
                finfo_close($finfo);
                return $mimetype;

            } else {

                return 'application/octet-stream';

            }
        }
    }
}

Keep secret files secret

If possible keep files such as database connection details outside of document root.

Don't use extension filenames like .inc

Unless the server is configured to specifically recognize an .inc file as a PHP file the server will display the file in plain text. If you absolutely must use .inc for included files use something like foo_inc.php or foo.inc.php.

I have never understood why someone would want to use the extension .inc just because it's an included file. Keep the extension .php!

Be mindful of shared hosting environments

Someone running a website on the same server as you may be able to reach your documents via his own website if the system is poorly configured. Hosting providers sometimes forget to isolate PHP installations. Shared hosting environments where all users are using the same temporary space makes it possible for malicious users to steal your session data. It also makes it impossible to change the default behavior of session timeouts.

Never trust ready made scripts or tutorials on the Internet

Get a firm grasp of the issue at hand!

There exists a lot of PHP scripts ready to download using Composer or GitHub with names like "Secure PHP login script", but they may not be secure.

Pulling things in randomly from GitHub or any other online repositories without code audit is irresponsible.

Always specify the correct charset with your HTML pages and database queries

You can do this in your php.ini file with the parameter default_charset = "UTF-8".

If you don't have access to php.ini you can include a PHP header on each page:

header('Content-Type: text/html; charset=UTF-8');

MySQL's encoding called "utf8" (alias of "utf8mb3") only stores a maximum of three bytes per code point. So the character set "utf8"/"utf8mb3" cannot store all Unicode code points: it only supports the range 0x000 to 0xFFFF, which is called the Basic Multilingual Plane. The real UTF-8 encoding needs up to four bytes per character. MySQL developers never fixed this, instead they released a workaround in 2010 called "utf8mb4".

Never use MySQL's "utf8", instead you should use "utf8mb4".

Here is an example for PHP PDO:

$host = '127.0.0.1';
$db   = 'test';
$user = 'root';
$pass = '';
$charset = 'utf8mb4';

$dsn = "mysql:host=$host;dbname=$db;charset=$charset";
$options = [
    PDO::ATTR_ERRMODE            => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES   => false,
];
try {
     $pdo = new PDO($dsn, $user, $pass, $options);
} catch (\PDOException $e) {
     throw new \PDOException($e->getMessage(), (int)$e->getCode());
}

Disable error reporting on production servers

Error reporting is nice and necessary in development, but once the system goes online any error reporting will only help attackers in gaining information about the website and underlying system.

Never use @ in front of functions in order to strangle error reporting. It makes it much harder to debug errors because errors then won't show during testing. Rather just turn off error reporting on production servers.

You can turn off error reporting like this:

<?php
error_reporting(0);
ini_set('display_errors', 0);

You can also force PHP to write errors to a log rather than the screen, but be careful not to count on this.

Writing a custom based error/exception handler is the preferred method.

During development make sure you use the highest level of error reporting.

In my opinion it is better to set error reporting "on" and "off" at runtime rather than using php.ini. This way you can make it a habit to not trust the web service provider to do it for you.

You can implement this by having PHP detect whether it is running on production or on testing.

Initialize variables

It is not necessary to initialize variables in PHP, but it is a very good programming practice. Especially because PHP's uninitialized variables have a default value of their type - false, zero, empty string or an empty array.

Some people do it like this:

// Initializing the variables.
settype($var1, "int");
settype($var2, "string");
settype($var3, "float");
settype($var4, "array");
settype($var5, "object");

But since PHP allows for type conversion I simply prefer to add some default content like this:

$intVar     = 0;
$stringVar  = 'init';

Turn of Magic Quotes

This feature has been DEPRECATED as of PHP 5.3.0 and REMOVED as of PHP 5.4.0.

The introduction of Magic Quotes in PHP was a huge mistake though originally the intend was good. Magic Quotes is a process that automatically escapes incoming data to the PHP script. Your should always code with Magic Quotes off and instead escape the data using the proper methods at runtime, as needed.

Not all data needs escaping, it's often annoying to see escaped data where it shouldn't be. For example emailing from a form and seeing a bunch of quotes within the email.

To fix this may require excessive use of stripslashes().

Make sure Magic Quotes are gone and don't worry about stripping slashes ever again.

If you are using a shared hosting environment you have to consider that the magic_quotes_gpc directive may only be disabled at the system level and not at runtime. In other words use of ini_set() is not an option.

Disabling magic quotes server side in php.ini:

; Magic quotes
;
; Magic quotes for incoming GET/POST/Cookie data.
magic_quotes_gpc = Off
; Magic quotes for runtime-generated data, e.g. data from SQL, from exec(), etc.
magic_quotes_runtime = Off
; Use Sybase-style magic quotes (escape ' with '' instead of ').
magic_quotes_sybase = Off

Register Globals

This feature has been DEPRECATED as of PHP 5.3.0 and REMOVED as of PHP 5.4.0.

PHP register_globals was a disaster.

When on, register_globals is an internal PHP setting that registers all the $_REQUEST array's elements as normal variables.

If you get any user input, via POST or GET, the value of that input will automatically be "transformed" into accessible variables in the PHP script. These variables will be named after the name of the input field.

If you submit a form containing a username text field the $_POST['username'] would automatically be equal to $username.

This opens up a lots of security holes, especially for people that isn't security aware when they are programming.

This combined with the fact that PHP doesn't require variable initialization means writing insecure code is that much easier.

Safe Mode is harmful

This feature has been DEPRECATED as of PHP 5.3.0 and REMOVED as of PHP 5.4.0.

Safe Mode is harmful because as it can lead to a false sense of security, but it rarely prevents access by a determined attacker.

Safe Mode is a blacklisting approach that restricts certain functions when it is enabled. According to the PHP manual:

The PHP safe mode is an attempt to solve the shared-server security problem. It is architecturally incorrect to try to solve this problem at the PHP level, but since the alternatives at the web server and OS levels aren't very realistic, many people, especially ISP's, use safe mode for now.

The core problem with Safe Mode is its inconsistency. In many situations, it works great and limits access to dangerous functions, however, all it takes is one allowed dangerous function to negate it completely.

The current best practice is to combine Safe Mode with a long list of functions for the "disabled_functions" parameter in the php.ini configuration file. This approach applies the Safe mode restrictions to PHP as a whole and then specifically limits functions that can be used to work around it. Again, the problem with this approach is inconsistency. If even a single dangerous function is missed, the entire process is useless. Depending on where you look on the Internet, the list of functions to disable is completely different.

In the event of a login failure, be very uncooperative

Don't give the user any information as to why the login failed other than "wrong user name and/or password". Log the real error message for scrutiny.

There is no need to provide a malicious user with information about whether or not the entered user name exists in the system.

The same goes for password reset features. Something like "A reset email has been send if the username exists" should suffice.

Further reading