PHP - Secure coding
From LXF Wiki
|Table of contents|
(Original version written by Paul Hudson for LXF issue 56.)
The Internet is the most fertile ground for malicious attacks ever invented, and it's also where most of our PHP scripts live. We look at how to protect your work...
Disabling fopen URLs might not be possible if you're using that functionality yourself, but otherwise it's strongly recommended.
Hardened PHP: the saviour of PHP programmers everywhere, or a bridge too far? Check out the site at www.hardened-php.net and decide for yourself.
Although it might not seem like it, it was two years ago this month that the Practical PHP tutorial kicked off - time flies when you're having fun, or something like that! Over the past two years we've looked at databases, multimedia, performance issues, process control, and various other topics to help hone your programming skills, but something we've not looked at yet - and this is perhaps an oversight on my behalf if nothing else! - is the topic of security. That is, how to write your scripts so that they cannot be exploited by malicious users.
As PHP is nearly always used to produce customer-facing front-ends, it does mean that most of the PHP code written is open to attack on two primary vectors: exploiting errors or omissions in your code, or exploiting security glitches in PHP itself. Of the two, the former is easier to prevent and is primarily accomplished by learning a small selection of guidelines to help keep your code safe. The latter may sound out of your control, but we're going to be looking at a couple of simple tricks that obscure PHP's presence on your system so that even if you're vulnerable to a PHP bug no one need ever know.
Programming secure PHP
Early versions of PHP, even up to v4.1 (released December 10th 2001), "helpfully" converted all incoming data into variables, whether that was system data, cookie data, session data, or straight user data such as form fields. This made programming with PHP particularly easy, but also particularly dangerous - failure to distinguish between trusted (system) and untrusted (user) data made exploiting many PHP scripts laughably easy.
However, as the language had been around for several years, changing this situation was going to be difficult: the PHP developers had the choice of leaving the status quo and having PHP scripts vulnerable to attack, or massively breaking backwards compatibility but making scripts much more secure. Unsurprisingly, the latter option was chosen and the register_globals php.ini setting was deprecated in PHP 4.1, which meant that it was left enabled (meaning that all data was automatically converted to variables) but users were warned against its use to give them time to migrate their scripts. Also introduced with PHP 4.x were the superglobal arrays $_GET, $_POST, etc, which became the preferred method of accessing data.
From PHP 4.2, register_globals was disabled by default, which meant that all programmers who wanted to stick to the standard php.ini settings (the vast majority) had to migrate their scripts to $_GET and $_POST if they wanted to upgrade. This was a painful move, but one that was definitely worth it: all new scripts are now produced using this more secure programming method, which helps keep attackers at bay. You can still enable register_globals if you really want to, and some do, but this does at least mean that system administrators need to explicitly remove the extra security.
Put key files outside your document root
Most Linux boxes have their document root - the root directory of their web site - as /var/www or something similar, which means that everything inside /var/www and its subdirectories are accessible by default over the web. While this makes permission control straightforward, many people store sensitive files inside this public directory, potentially making them accessible to the world. One of the most striking examples of why this is bad practice is "Google hacking" - the act of using Google to drag up otherwise-hidden information that Google stumbled across and cached.
Although it's possible to not link to these sensitive files from anywhere, and you can also easily give them hard to guess filenames (hint: "dbconnect.php" isn't a smart filename!), a much better option is to put these sensitive documents outside the document root of your web server so that only scripts on your site can load them. For example, if your document root is /var/www/html, you could put these key documents in /var/www and keep them safe from the outside world.
Choose your file extension carefully
PHP can parse any valid script, whether it's called foo.php, very_long_name.php.php.php, or even wom.bat. Using the default extension of ".php" means that before hackers start you've already told them you're using PHP. If you're using PHP for every script on your server, consider using the ".html" extension for your scripts and making PHP parse HTML files. To external users it will look like you're running static HTML, but internally it works just the same. If you really want to confuse hackers, try using the ".asp" extension usually seen on Microsoft web servers!
If you're running Apache, you can change your file extension by changing this line:
AddType application/x-httpd-php .php
The .php part can be changed to .html, .foo, or whatever else you want - be creative!
Keep PHP scripts executable
Once you've chosen the file extension for your PHP scripts, stick to it. A large number of programmers coming from other languages try to import their filing rules directly into PHP, which resulted in them using the file extension ".inc" for "include" files - scripts that only served to be included into other scripts. While this certainly allows you to distinguish include files from non-include files simply by looking at a directory listing, it's actually a major security hole.
For example, if you save your database connection info in a file, then include() that file into every script you write, that file would probably be called something like dbconnect.inc if you followed this naming convention. Now, what happens if someone were to type www.yoursite.com/dbconnect.inc directly into their web browser? Your web server would load the ".inc" file, and send it as plain text because it doesn't end in a PHP-handled file extension, which means that someone accessing the .inc file directly would see your source code.
A much better solution, if you particularly want to mark your files as include files, is to use the extension .inc.php - this way, they will be parsed by PHP before being sent to people directly, and therefore won't reveal your source code, whilst at the same time clearly marking them as include files.
Hide your identity
Most web servers, by default, send out information about themselves with each request served. For example, a default installation of Mandrake Linux 10.0 returns the following information with each file served:
Server: Apache-AdvancedExtranetServer/2.0.48 (Mandrake Linux/6.1.100mdk) mod_perl/1.99_11 Perl/v5.8.3 mod_ssl/2.0.48 OpenSSL/0.9.7c PHP/4.3.4
From that we can ascertain that the machine is running Apache 2.0.48 ("Advanced Extranet Server" is Mandrake's name for their modified version of Apache), along with mod_perl, mod_ssl, and PHP 4.3.4.
Now, all an attacker has to do is check for known bugs in Apache 2.0.48 or PHP 4.3.4. As both of these have been out for a very long time, there are likely several known exploits in there, of which at least one may well be /remotely/ exploitable. Many malicious users make use of automated version scanners that trawl through the web looking for specific version numbers of Apache or its plugins and compile lists of vulnerable machines.
Open up your httpd.conf file, and look for the two directives "ServerSignature" and "ServerTokens". Both of these control what information Apache gives out about itself, and are set by default to send out comprehensive information. ServerSignature is used to define what Apache prints at the bottom of server-generated pages such as 404 error pages.
Similarly, with ServerTokens set to full (the default), the same information is sent along with every request. To change this, set ServerSignature to "Off" and ServerTokens to "Prod" - this will stop it printing anything out for error messages, and restrict the information sent with each request to just "Apache". A big step forward - at least now your site won't appear if people are scanning for certain Apache versions.
By default, PHP is set to announce its presence whenever anyone asks - this is usually through the web server. As discussed, you can turn this off using ServerTokens and ServerSignature, but if you'd rather not throw the baby out with the bath water you can be a little more selective about how modules report themselves. For example, if you leave ServerTokens and ServerSignature on, you can still hide PHP's existence by changing "expose_php" to "Off" in php.ini - this leaves most server information showing, but hides the PHP data.
If you do this, as well as using a different file extension, your use of PHP is mostly hidden. However, if your code generates any error messages, your use of PHP will become immediately obvious. To get around this, and thereby truly hiding PHP, you should force PHP not to display error messages - edit your php.ini file and set "display_errors" to "Off". This will make debugging a little harder, but be sure to set "log_errors" to "On" - this will make sure that whenever your script generates an error, it will be stored away in the error log file so that you can analyse the problem at your leisure.
Restrict database access
Although the PHP code that drives your site may well be unique and of value to your company, it's likely that the database behind it is much more important and should be treated as such. MySQL's access control is very finely grained, and gives you a great deal of control over who can do what. Even so you should take advantage of this to make sure you only allow in people you absolutely trust. The first step in this process is to remove the guest account, leaving only the root user plus any others you use. Secondly, if you're running your server locally and the PHP scripts are local also, you don't need to allow access to anyone from outside - disable accounts that don't have "localhost" as the host. Finally, consider blocking port 3306 (the MySQL port) on your firewall so that there's one less possible way into your system.
You can also rethink how your PHP scripts connect to MySQL - most people go for one of two options: write database connection code into each of your scripts, or write it into just one script and link all your pages to that one. The latter is usually preferred as it makes life easier when you change your connection password, but it does mean that you're putting all your eggs in one basket as it were. Fortunately there is a third option that can sometimes be better: placing your connection details inside your php.ini file. If you don't supply connection details to mysql_connect(), PHP will use the values set in your php.ini file, which means you don't need to store your username and password information in your scripts any more. At first this might sound perfect, but it has major security implications of its own:
- Anyone with access to your php.ini can read the values direct from the file - Anyone with the ability to put scripts on your server can use the ini_get() function to read the value from your php.ini file
If you firmly believe you're safe from these two, then go ahead and use your php.ini file.
Finally, don't forget that the fine-grained access control of MySQL means it's easy to use multiple usernames and passwords to segment security on your server - having one set of credentials for your news database, another for your forums, and so on, means that even if somehow your site gets hacked there is some degree of damage limitation.
Denial of service
Although I don't want to discourage you, it is actually remarkably easy for malicious users to take down your site even if your code is perfect and PHP itself is patched up to date. Denial of service (DoS) is the term for people attacking your site to make it either run slow or come offline entirely, and there are three vectors:
- A malicious user with a fast Internet connection bombards your web server with requests, thereby overloading it - A malicious user with accomplices, who may be unwitting, bombard your web server with requests, thereby overloading it. In this situation the attackers don't need fast Internet connections - 100 requests from 10,000 people is more damaging than 1,000,000 requests from one person. - A malicious user finds a hole in your web site that forces your server to perform an inordinate amount of work, thereby overloading the server.
Of the three, the first two are impossible to defend against - the world's largest sites have been taken offline by these form of denial of service, and there's nothing you can do whether or not you're using PHP. The last option, however, /is/ something you can guard against. If you have holes in your code that can be exploited by outsiders to cause your web server to chew up 99% of your CPU time, this is a legitimate security issue.
A popular mistake is to write code that results in URLs like this:
www.example.com/article.php?file=aboutus.php www.example.com/article.php?file=products.php www.example.com/article.php?file=legal.php
The code for article.php will basically read in the $_GET['file'] variable, then include() the necessary file into the script. This might make sense at first, but consider what happens if a clever use modified the URL to this:
What will happen is that article.php will load, then include() article.php, which will load, then include() article.php, which will load, then... and so on. This will continue going on and on until your server hits the maximum execution time for a script and terminates. However, during this time your web server will be performing large amounts of unnecessary work, and will be slower for other clients connecting to it.
Now consider what would happen if that same malicious user loaded that URL three times quickly - or thirty. From that, consider what would happen if that user loaded the URL three thousand times, which is nothing difficult considering that can be handled even with a slow connection using a HTTP HEAD request. At three thousand almost-simultaneous connections, even a normal web server would have trouble coping. However, if each of those three thousand resulted in a CPU-consuming infinite include() loop, the server would simply stop responding to new requests and may well even crash.
The moral of the story is that you should always keep in mind the possibility that malicious users may use your own code against you. The most obvious solution to the problem detailed here is not include files based upon a variable, but if that's not possible then at least consider using include_once() to stop the possible of recursion.
Even with this fix in place, it's still not a smart idea to advertise so openly that you are including files to get your content. For example, a malicious user could rewrite your URL to this:
That would cause your server to connect to an external site to get its code, which essentially allows the evil hacker to execute whatever code they please on your site. This form of attack can be stopped in its tracks by editing your php.ini file and setting allow_url_fopen to Off.
Hosting PHP scripts is pretty much an invitation for your users to abuse their privileges, and it's remarkably easy to do with PHP. Fortunately for us, the PHP developers took this situation into account and created /safe mode/ - a setting that can be toggled in the php.ini file, which, when enabled, applies various lockdowns to the language. For example, by default safe mode blocks the dl() function, as it could potentially be used by attackers to load an unsafe extension for execution.
By default, PHP running in safe mode will only work with files that are owned by the same person who owns the script that's being run - the user ID (UID) of the owner of the script must match the UID of the owner of the file being read. This includes files being read through fopen(), and even files being read through an include call. In addition, there are several settings in your php.ini file that are likely to be of help if you're trying to secure your PHP environment:
- safe_mode_include_dir - this defines a directory you consider safe on your computer, where all files can be worked with regardless of their ownership. Files read from this directory don't have their UID checked against the owner of the script.
- safe_mode_exec_dir - this defines a directory from which you want PHP to be able to execute programs while running in safe mode. If this isn't set, all calls to exec() will fail.
- safe_mode_allowed_env_vars - this defines a list of environment variables that the user will be allowed to change. If this is not set, all variables can be edited, which is not likely to be a good thing if abused.
- open_basedir - this setting allows you to limit the location from where files can be read, thereby stopping people from reading in any files they please. This is a tricky setting to get right, and is discussed in more depth below.
- disable_functions - this setting does precisely what you'd expect it to - provide it with a list of functions you don't want to be used, and it will automatically stop scripts from using them. Specify multiple functions with commas, for example: readfile,exec,fopen.
- disable_classes - this takes a list of classes you don't want people to create objects from, and stops them being created, as you'd expect. As with disable_functions, use commas to separate multiple class names.
The settings are generally easy to grasp, as you can see, however open_basedir is a little more complicated than it first seems. For example, open_basedir will work regardless of whether safe mode is enabled, whereas the others only kick in when PHP is operating in safe mode. Secondly, the directories you pass in to this directive, separated by commas, are considered to be prefixes by default. For example, /home/paul will allow files in /home/paul to be loaded, but also in /home/paul_the_hacker, etc. To clarify a particularly directory exactly, add a slash to the end. For example: /home/paul/ would only match the directory /home/paul.
Finally, note that this directive resolves all links. For example, if file /home/paul/passwd is symlinked to /etc/passwd, and open_basedir has been used to restrict file inclusion to /home/paul/, including /home/paul/passwd would fail - PHP would detect that it linked to a file stored in /etc, outside of the open_basedir path, and prevent the call from continuing.
Note that many Linux distributions backport security fixes to their stable release of Apache and its modules. For example, although Red Hat Enterprise Linux 3 ships with Apache 2.0.46, it incorporates backports of all the security fixes introduced in 2.0.47, 2.0.48, and 2.0.49. As a result, you cannot rely solely on the hard-coded version number to tell you whether you have the latest release or not - check with your vendor for more information.
If you want to take your PHP security a step further and you compile your own version from the source code as opposed to using pre-built binaries, you can apply a special set of patches called Hardened PHP that toughen up PHP's internals to make them more robust. For example, it runs so-called "canary checks" that ensure buffer overflows are spotted and stopped before they can cause problems, but it also monitors the Zend Engine's memory management routines to make sure all memory is allocated and freed safely.
Although it does undoubtedly improve the security of PHP as a whole, we'd probably not recommend Hardened PHP to everyone - unless you're really paranoid, Hardened PHP is best left to environments with shared resources, such as shared hosting web servers.