PHP Security

Posted on 28.11.2008 by Kim N. Lesmer. Last updated on 05.04.2015.
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.

If you find anything important missing in this document please let me know!

Always write your applications with security in mind!

Table of Contents

  1. Introduction
  2. Golden rules
  3. Links

Introduction

When you use PHP or any other programming language you have to think about security.

Thinking about security isn't something we tend to do when we write code, it is rather something that we need to get into the habit of thinking about.

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 such functionality that's best to avoid using.

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

  • Never ever trust users - not even employees.
  • Try to understand the systems that your applications run on.
  • Understand what your application does and how it can be manipulated so that it gives unauthorized users access to forbidden areas or allows for malicious actions to take place.
  • Understand how your application can be changed or manipulated into doing things it wasn't originally supposed to do. How can the user manipulate your application in ways you didn't plan for?
  • Nothing is totally secure. Segment your software to limit the damage if it is compromised.

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.

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 or some other means of protection. Prepared statements uses placeholders for the input data making it impossible for the database to interpret any of the values. This completely prevents SQL injection.

It's worth to mention that data that goes into the database shouldn't be changed. Meaning: Don't create or use functions that add slashes to the data in such a way that you have to remove slashes from the data when it is fetched from the database. The process of escaping data must also preserve the data so it should never be necessary to reverse it. Functions such as stripslashes() demonstrates a bad design.

Using prepared statements such as PHP PDO preserves the data completely so you don't need to use something like stripslashes() when you retrieve the data from the database.

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

Also don't trust on addslashes().

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 user agent and is then feeded to the server which feeds it back to the PHP script. $_SERVER['HTTP_HOST'], as an example, is from the user agent.

Always escape output

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

Any display output to the browser should 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 and 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 insertion of for example Javascript that can be used in a number of Cross-site Scripting attacks.

Be careful when including files

Don't ever load or read files using any of the different include functions if the filename or the file itself is depending on 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 or 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 secret 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.

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

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.

Not only that but you also actually have to validate that the code you're using isn't malicious. Pulling things in randomly from Github or any other online repository 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');

Depending on what database you are using the specification for charset is different. On MySQL using PHP PDO you can use the following example:

$pdo = new PDO('mysql:host=hostname;dbname=defaultDbName', 'username', 'password',
    array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8")
);

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.

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 of not counting on the web service provider to do it for you.

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.

Initialize variables

It is not necessary to initialize variables in PHP, but it is a very good practice. Especially because PHP 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 thus 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 away any information as to why the login failed other than "wrong user name and/or password".

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

Further reading

The PHP Manual on Security

OWASP PHP Security Cheat Sheet

A really good article about Cross-Site Scripting by Chris Shiflett's

Security Focus

Social engineering