Flylib.com

Books Software

 
 
 

12.3 File Uploads


12.3 File Uploads

File uploads combine the two dangers we've seen so far: user -modifiable data and the filesystem. While PHP 4 itself is secure in how it handles uploaded files, there are several potential traps for unwary programmers.

12.3.1 Distrust Browser-Supplied Filenames

Be careful using the filename sent by the browser. If possible, do not use this as the name of the file on your filesystem. It's easy to make the browser send a file identified as /etc/passwd or /home/rasmus/.forward . You can use the browser-supplied name for all user interaction, but generate a unique name yourself to actually call the file. For example:

$browser_name = $_FILES['image']['name'];
$temp_name = $_FILES['image']['tmp_name'];
echo "Thanks for sending me $browser_name.";

$counter++; // persistent variable
$my_name = "image_$counter";
if (is_uploaded_file($temp_name)) {
  move_uploaded_file($temp_name, "/web/images/$my_name");
} else {
  die("There was a problem processing the file.");
}

12.3.2 Beware of Filling Your Filesystem

Another trap is the size of uploaded files. Although you can tell the browser the maximum size of file to upload, this is only a recommendation and it cannot ensure that your script won't be handed a file of a larger size . The danger is that an attacker will try a denial of service attack by sending you several large files in one request and filling up the filesystem in which PHP stores the decoded files.

Set the post_max_size configuration option in php.ini to the maximum size (in bytes) that you want:

post_max_size = 1024768        ; one megabyte

The default 10 MB is probably larger than most sites require.

12.3.3 Surviving register_globals

The default variables_order processes GET and POST parameters before cookies. This makes it possible for the user to send a cookie that overwrites the global variable you think contains information on your uploaded file. To avoid being tricked like this, check the given file was actually an uploaded file using the is_uploaded_file( ) function.

In this example, the name of the file input element is "uploaded":

if (is_uploaded_file($_FILES['uploaded_file']['tmp_name'])) {
  if ($fp = fopen($_FILES['uploaded_file']['tmp_name'], 'r')) {
    $text = fread($fp, filesize($_FILES['uploaded_file']['tmp_name']));
    fclose($fp);

    // do something with the file's contents
  }
}

PHP provides a move_uploaded_file( ) function that moves the file only if it was an uploaded file. This is preferable to moving the file directly with a system-level function or PHP's copy( ) function. For example, this function call cannot be fooled by cookies:

move_uploaded_file($_REQUEST['file'], "/new/name.txt");

12.4 File Permissions

If only you and people you trust can log into your web server, you don't need to worry about file permissions for files created by your PHP programs. However, most web sites are hosted on ISP's machines, and there's a risk that untrusted people will try to read files that your PHP program creates. There are a number of techniques that you can use to deal with file permissions issues.

12.4.1 Get It Right the First Time

Do not create a file and then change its permissions. This creates a race condition, where a lucky user can open the file once it's created but before it's locked down. Instead, use the umask( ) function to strip off unnecessary permissions. For example:

umask(077);            // disable ---rwxrwx
$fp = fopen("/tmp/myfile", "w");

By default, the fopen( ) function attempts to create a file with permission 0666 ( rw-rw-rw- ). Calling umask( ) first disables the group and other bits, leaving only 0600 ( rw------- ). Now, when fopen( ) is called, the file is created with those permissions.

12.4.2 Session Files

With PHP's built-in session support, session information is stored in files in the /tmp directory. Each file is named /tmp/sess_ id , where id is the name of the session and is owned by the web server user ID, usually nobody .

This means that session files can be read by any PHP script on the server, as all PHP scripts run with the same web server ID. In situations where your PHP code is stored on an ISP's server that is shared with other users' PHP scripts, variables you store in your sessions are visible to other PHP scripts.

Even worse , other users on the server can create files in /tmp . There's nothing preventing a user from creating a fake session file that has any variables and values he wants in it. The user can then have the browser send your script a cookie containing the name of the faked session, and your script will happily load the variables stored in the fake session file.

One workaround is to ask your service provider to configure their server to place your session files in your own directory. Typically, this means that your VirtualHost block in the Apache httpd.conf file will contain:

php_value session.save_path /some/path

If you have .htaccess capabilities on your server and Apache is configured to let you override Options, you can make the change yourself.

For the most secure session variables possible, create your own session store (e.g., in a database). Details for creating a session store are given in Chapter 7.

12.4.3 Don't Use Files

Because all scripts running on a machine run as the same user, a file that one script creates can be read by another, regardless of which user wrote the script. All a script needs to know to read a file is the name of that file.

There is no way to change this, so the best solution is to not use files. As with session stores, the most secure place to store data is in a database.

A complex workaround is to run a separate Apache daemon for each user. If you add a reverse proxy such as Squid in front of the pool of Apache instances, you may be able to serve 100+ users on a single machine. Few sites do this, however, because the complexity and cost are much greater than those for the typical situation, where one Apache daemon can serve web pages for thousands of users.

12.4.4 Safe Mode

Many ISPs have scripts from several users running on one web server. Since all the users who share such a server run their PHP scripts as the same user, one script can read another's data files. Safe mode is an attempt to address this and other problems caused by shared servers. If you're not sharing your server with other users that you don't trust, you don't need to worry about safe mode at all.

When enabled through the safe_mode directive in your php.ini file, or on a per-directory or per-virtual host basis in your httpd.conf file, the following restrictions are applied to PHP scripts:

  • PHP looks at the owner of the running script and pretends [1] to run as that user.

    [1] PHP can't switch the user ID via a setuid( ) call because that would require the web server to run as root and on most operating systems it would be impossible to switch back.

  • Any file operation (through functions such as fopen( ) , copy( ) , rename( ) , move( ) , unlink( ) , chmod( ) , chown( ) , chgrp( ) , mkdir( ) , file( ) , flock ( ) , rmdir( ) , and dir( ) ) checks to see if the affected file or directory is owned by the same user as the PHP script.

  • If safe_mode_gid is enabled in your php.ini or httpd.conf file, only the group ID needs to match.

  • include and require are subject to the two previous restrictions, with the exception of include s and require s of files located in the designated safe_mode_include_dir in your php.ini or httpd.conf file.

  • Any system call (through functions such as system( ) , exec ( ) , passthru ( ) , and popen( ) ) can access only executables located in the designated safe_mode_exec_dir in your php.ini or httpd.conf file.

  • If safe_mode_protected_env_vars is set in your php.ini or httpd.conf file, scripts are unable to overwrite the environment variables listed there.

  • If a prefix is set in safe_mode_allowed_env_vars in your php.ini or httpd.conf file, scripts can manipulate only environment variables starting with that prefix.

  • When using HTTP authentication, the numerical user ID of the current PHP script is appended to the realm [2] string to prevent cross-script password sniffing, and the authorization header in the getallheaders( ) and phpinfo( ) output is hidden.

    [2] This realm-mangling took a little vacation in PHP 4.0.x but is back in PHP 4.1 and later.

  • The functions set_time_limit( ) , dl( ) , and shell_exec( ) are disabled, as is the backtick ( `` ) operator.

To configure safe_mode and the various related settings, you can set the serverwide default in your php.ini file like this:

safe_mode = On
safe_mode_include_dir = /usr/local/php/include
safe_mode_exec_dir = /usr/local/php/bin
safe_mode_gid = On
safe_mode_allowed_env_vars = PHP_
safe_mode_protected_env_vars = LD_LIBRARY_PATH

Alternately, you can set these from your httpd.conf file using the php_admin_value directive. Remember, these are system-level settings, and they cannot be set in your .htaccess file.

<VirtualHost 1.2.3.4>
  ServerName domainA.com
  DocumentRoot /web/sites/domainA
  php_admin_value safe_mode On
  php_admin_value safe_mode_include_dir /usr/local/php/include
  php_admin_value safe_mode_exec_dir /usr/local/php/bin
</VirtualHost>