About the Author

Chris Shiflett

Hi, I’m Chris: entrepreneur, community leader, husband, and father. I live and work in Boulder, CO.


File Uploads

  • Published in PHP Architect on 18 Oct 2004
  • Last Updated 18 Oct 2004
  • 10 comments

Welcome to another edition of Security Corner. This month's topic is file uploads, and I focus on the mechanism you create to allow users to upload files to your application. Unlike typical form data, files are handled uniquely, and PHP uses the $_FILES array to provide you with all of the information you need. However, because it isn't very clear what information is provided by the client and what information is provided by PHP, a security-conscious developer can have a difficult time determining what data can be trusted.

This article takes a detailed look at file uploads, beginning with a brief discussion that walks you through the process and some example code that offers this feature. This is followed by a close examination of the mechanics of file uploads, and then I discuss the security risks inherent in this activity as well as some safeguards and best practices that you can implement in your own applications.

File Uploads

In order to let users upload files, you must present them with a typical HTML form. However, because files are not sent in the same way that regular form data is, you must specify a particular encoding:

<form action="upload.php" method="POST" enctype="multipart/form-data">

The enctype attribute is often left out, so you might not be familiar with it. An HTTP request that includes both regular form data and files has a special format, and this attribute is necessary for the browser's compliance.

The form field for a file is actually very simple:

<input type="file" name="attachment" />

This is rendered in various ways by the different browsers. Traditionally, the interface includes a standard text field as well as a browse button, so that the user can either enter the path to the file manually or browse for it. In Safari, only the browse option is available. Regardless, the behavior from a developer's perspective is the same, but you might want to be mindful of the differences in presentation, in case you have very specific instructions for the user.

To better illustrate a file upload, I present you with an example HTML form that can be used to allow users to upload attachments to a web-based email application:

<p>Please choose a file to upload:</p>
<form action="upload.php" method="POST" enctype="multipart/form-data"> 
<input type="file" name="attachment" /> 
<input type="submit" value="Upload Attachment" /> 
</form>

The PHP directive upload_max_filesize can be used to limit the size of uploaded files, and post_max_size can potentially restrict this as well, because the file is part of the POST data.

Figure 1 shows how this form appears when rendered in a browser.

When this form is submitted, the HTTP request is sent to upload.php. In order to demonstrate what information is made available to you, upload.php does the following:

<?php
 
header('Content-Type: text/plain');
print_r($_FILES);
 
?>

To experiment with my form, I choose to upload the September 2004 issue of php|architect, which is a file on my computer named phpa_09-2004.pdf. Upon selecting this article and submitting the form, I get the following response from upload.php:

Array
(
    [attachment] => Array
        (
            [name] => phpa_09-2004.pdf
            [type] => application/pdf
            [tmp_name] => /tmp/phpz1A0zr
            [error] => 0
            [size] => 2704632
        )
 
)

This shows exactly what information PHP provides in the $_FILES superglobal array. However, what it doesn't show is what information can be trusted. With a cursory glance, you probably suspect that the name is provided by the client, but what about the other information?

Multipart HTTP Request

In order to clarify things, it is necessary to examine the HTTP request, because this shows you exactly what is sent by the client. Because the September 2004 issue of php|architect is a rather large file, I use something much smaller in this next example. Using the same form, I can upload a file named attachment.txt that contains the following:

Security Corner: File Uploads
Chris Shiflett
php|architect
Oct 2004

When I upload this file, I see the following:

Array
(
    [attachment] => Array
        (
            [name] => attachment.txt
            [type] => text/plain
            [tmp_name] => /tmp/phpnXMOeW
            [error] => 0
            [size] => 68
        )
 
)

The HTTP request sent by my browser is as follows (some optional headers are removed for brevity):

POST /upload.php HTTP/1.1
Host: example.org
Referer: http://example.org/attach.php
Content-Type: multipart/form-data; boundary=---------------------------11401160922046879112964562566
Content-Length: 298
 
-----------------------------11401160922046879112964562566
Content-Disposition: form-data; name="attachment"; filename="attachment.txt"
Content-Type: text/plain
 
Security Corner: File Uploads
Chris Shiflett
php|architect
Oct 2004
 
-----------------------------11401160922046879112964562566--

It is not necessary to understand the format of this request, but it should be easy to spot the contents of the file and its associated metadata. The name attribute is the name of the form field given in attach.php, and the filename attribute is the name of the local file on the user's computer. Based on this example request, it seems that only the name and type are potentially dangerous, because it's difficult for an attacker to do much damage by changing the name of the form field itself, primarily because your code references this by name (e.g., changing the name will usually result in the code accessing a variable that does not exist, and the only caveat is when the code loops through all form data for some reason).

In order to better appreciate what an attacker can accomplish, use the code in Listing 1 to perform your own tests. You will need to change the domain name referenced in the Host header as well as make sure /upload.php exists on your server. If you can figure out a way to alter the tmp_name or size, you can discover a dangerous opening for an attacker.

Practical Risks

Surprisingly, there are very few additional risks associated with file uploads, although it is important to remember the risks associated with any form processing - you have no assurance as to the format or size of anything sent in each request.

Validating the format of a file depends entirely on your specific application, and binary files present additional challenges. Although I do not discuss these approaches here, anti-virus software, file signatures, and the like can be used to help prevent certain malicious file types. Although these are blacklist approaches and fundamentally flawed, they may be your only option.

The filename (attachment.txt in my example) is provided by the client, and it should be filtered before being used in any capacity. If your requirements allow it, you can ignore this information completely and choose your own name. This eliminates this particular risk entirely.

Theoretical Risks

There are two things in $_FILES for which you want to implement additional safeguards: tmp_name and size. While I have been unable to uncover a specific exploit in my research (I am limited to a small number of platforms), there are best practices available that prevent the theoretical attacks that involve the client being capable of manipulating this information.

In order to be assured that the filename given in tmp_name is actually the file that was uploaded with the form (and not an arbitrary file given by the user, such as /etc/passwd), you can use is_uploaded_file() as follows:

<?php
 
$filename = $_FILES['attachment']['tmp_name'];
 
if (is_uploaded_file($filename)) { 
    /* $filename is an uploaded file. */
}
 
?>

This is particularly important in situations where some or all of the contents of the uploaded file are displayed to the user (perhaps for verification).

If you plan to simply move this file to another location in the filesystem, PHP provides you with a function that first checks whether the file given is an uploaded file and only moves it if it is:

<?php
 
$old_filename = $_FILES['attachment']['tmp_name'];
$new_filename = '/path/to/attachment.txt';
 
if (move_uploaded_file($old_filename, $new_filename)) { 
    /* $old_filename is an uploaded file, and the move was successful. */
}
 
?>

If you want to be sure of the file's size, you can use a standard PHP filesystem function after verifying that the file is a valid uploaded file:

<?php
 
$filename = $_FILES['attachment']['tmp_name'];
 
if (is_uploaded_file($filename)) { 
    $size = filesize($filename);
}
 
?>

Until Next Time...

It might seem unnecessary to validate data when you are not aware of an exploit that lets the client modify it. This approach of adding redundant safeguards is known as defense in depth, and I highly recommend abiding by it. Theoretical attacks have been known to materialize into real attacks, and you'll be glad that you're prepared.

If you happen to find a way to modify the tmp_name or size attribute of a file, with register_globals, please let me know.

You can now eliminate file uploads from your list of worries, and I hope that I've also been able to provide some clarity regarding the underlying mechanism. Until next month, be safe.

About this article

File Uploads was last updated on 18 Oct 2004. Follow me on Twitter.

10 comments

1.Enquest wrote:

You explain how to upload a file but not how to secure the uploaded file. What if somebody uploads a .php file somehow. Or if you put the file out of your www folder and acces it the wrong way. There is the real security problem for beginning programmers

Tue, 11 Oct 2005 at 07:53:47 GMT Link


2.bzikofski wrote:

excellent article !!!

will there be a .pdf version of this ?

(i can make it myself, i'm just being lazy today ;)

Tue, 11 Oct 2005 at 09:04:52 GMT Link


3.bzikofski wrote:

Enquest: the title of this article is "File Uploads" not "File Upload Security" ...

Tue, 11 Oct 2005 at 09:21:16 GMT Link


4.Furia wrote:

How can I restrict the file type? Only .txt for example.

Thanks in advance.

Wed, 28 Dec 2005 at 01:51:40 GMT Link


5.Jakob B. wrote:

A very simple solution would be.

<?php

function isAllowedFile($filename) {

$filetypes = array ('.txt','.jpg');

return in_array(strrchr($filename,'.'),$filetypes);

}

if (isAllowedFile('index.inc.txt'))

echo 'okay! filetype is allowed';

else

echo 'error! not allowed!';

?>

You can also check the mime type i.e.

<?php

if ($_FILES['userFile']['type'] === 'text/plain')

?>

Thu, 29 Dec 2005 at 13:09:20 GMT Link


6.James Benson wrote:

Their are so many bad example out their it's amazing, I literally cannot count the amount of times I've seen code that does not check using the PHP function and uses copy instead of move_uploaded_file, keep up the good work!

Fri, 09 Jun 2006 at 22:13:53 GMT Link


7.Oren wrote:

There is a small problem with Jakob's function; it will return FALSE for a file named: foo.bar.txt

In the above example (foo.bar.txt) strchr() will return .bar.txt, and therefore a valid .txt file won't be recognized as valid, although it is.

Chris - the title of this article is 'Security Corner: File Uploads' and it suggests (at least for me) that it will be about secure uploads not simply uploading files...

Anyway, keep on with the good work you are doing (even though this article didn't teach me something I hadn't known before).

Thanks Chris,

Oren

Mon, 10 Jul 2006 at 18:27:54 GMT Link


8.Craig Francis wrote:

Just adding to 'Enquest' comment, once the file is uploaded, you still need to treat is as dangerous / un-trusted.

You cannot trust the file extension, so sorry 'Jakob B', your filter can be bypassed.

Also the MIME can also be a little unreliable, as you might not cater for all possibilities... for example, JPEG images usually appear as "image/jpeg"... but you should also consider "image/jpeg2000" and "image/pjpeg" (progressive JPEG)... same with "image/png" and "image/x-png"

One solution I use when dealing with images is to:

1) Check is_uploaded_file()

2) Use getimagesize() to determine the image type.

3) Open the file with the GD functions imagecreatefrom*();

4) Perform any image sizing (if required).

5) Save out the new file with something like imagejpeg();

And even then, you cannot be sure the images uploaded to a public gallery does not contain content that is inappropriate for your visitors.

The main reason you should re-save the image is because Internet Explorer has a 'feature' where it tries to guess the content of a file, and will quite happily process the file as HTML/JavaScript if it can find a <script> tag within the image file... with the added bonus that there has been a couple of buffer overflow issues with specially crafted images (PNG, JPEG, GIF, etc).

See 'The hazards of MIME sniffing' by 'Wladimir Palant' for more information.

Sat, 19 May 2007 at 13:54:33 GMT Link


9.Azirius wrote:

I find the best way to check the file type is like so...

<?php
 
$ext = ".".array_pop(explode('.', $_FILES['name']));
 
$allowed_ext = array(
 
'.html',
 
'.jpg'
 
);
 
if(in_array($ext, $allowed_ext)) {
 
// valid ext
 
}
 
?>

That should always get the proper extension.

Sun, 13 Apr 2008 at 16:26:57 GMT Link


10.Arran Schlosberg wrote:

Thanks Chris.

Jakob and Azirius: as explained in the article the extension is set by the client and is thus useless for checking file validity. So too is the mime type (IE even provides non-conventional types for some files which causes major headaches).

Your setup is the only system that is consistent and in your control so rather use myme_content_type() (deprecated) or the recommended FileInfo.

There are risks involved in "hybrid files" that return particular mime types but include other code (e.g. the TIFF that jailbreaks iPod touches). I'm no expert though so I'm not sure of all associated risks from these files as well as anything else I don't know about.

Thu, 02 Apr 2009 at 23:13:23 GMT Link


Hello! What’s your name?

Want to comment? Please connect with Twitter to join the discussion.