Hi ,
I'm Kamal Hinduja based in Geneva, Switzerland. Can anyone explain How to set up file uploads in PHP securely?
Thanks, Regards
Kamal Hinduja Geneva, Switzerland
Hi ,
I'm Kamal Hinduja based in Geneva, Switzerland. Can anyone explain How to set up file uploads in PHP securely?
Thanks, Regards
Kamal Hinduja Geneva, Switzerland
Jump to Post— IS-92 70FILE UPLOAD VULNERABILITIES
The first vulnerability I'd like to discuss is insufficient filename handling during uploads. This vulnerability is marked as private on bugs.php.net, but if you dig around, you can still find a description.
The bug is that if a filename begins with a slash or backslash and contains …
Jump to Post— IS-92 70"SAFE" FILE UPLOADING TO THE SERVER
As noted above, file uploading is primarily accomplished using the moveuploadedfile and copy functions. However, there are other options for accomplishing this complex and demanding task. One such option (which, by the way, is preferable if we're only uploading images) is to use the …
Jump to Post— fabwebstudio 37To set up secure file uploads in PHP, follow these best practices:
Validate the file type – Always check the MIME type and file extension using mime_content_type() or finfo_file() to ensure only allowed formats (e.g., images, PDFs) are uploaded.
Limit file size – Set a maximum file size using MAX_FILE_SIZE …
Hello and welcome to DaniWeb! Although curt, brings up a good point. It’s pretty impossible for us to give you actionable advice with such a broad question. What is your use case? What have you done so far? Can we see your code so we can make security recommendations?
I have been trying to figure this out too. From what i read you really need to check file types limit file size, and store uploads outside the web root if possible. Also, giving files unique names helps avoid overwriting. Has anyone tried a specific method that worked well for them?
I would recommend a PHP uploads library off of Github that takes care of all of the security-related minutia for you.
I use the Codeigniter PHP framework for DaniWeb and it comes with an uploads library. Zend, laravel, etc. come with their own as well.
Hi Kamal,
Reliable file uploading to a server requires a combination of configuration, code processing, and security measures. File uploads can pose significant risks, including malicious file uploads, server overload, and data leaks.
To answer the question posed in this way, I would have to write an entire monograph.
However, let's examine a specific example related to the secure uploading of digital images using PHP scripts.
EXAMPLE 1:
Checking the Content of an Image File
Instead of relying on the Content-Type header, a PHP developer can check the actual content of the uploaded file to make sure it is indeed an image. The PHP function getimagesize() is often used for this purpose. It takes the file name as an argument and returns an array of image sizes and types.
Consider the upload3.php example below.
<?php
$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 "File is valid, and was successfully uploaded.\n";
} else {
echo "File uploading failed.\n";
}
?> Now, if an attacker tries to upload shell.php, even if they set the Content-Type header to "image/gif", upload3.php will still return an error.
EXAMPLE 2:
Checking the Upload File Extension
Why don't we simply check the uploaded file extension?
If we don't allow *.php files to be uploaded, the server will never be able to execute that file as a script. Let's consider this approach.
We can create a blacklist of file extensions and check the uploaded file name, ignoring uploads of files with executable extensions (upload4.php):
<?php
$blacklist = array(".php", ".phtml", ".php3", ".php4");
foreach ($blacklist as $item) {
if(preg_match("/$item\$/i", $_FILES['userfile']['name'])) {
echo "We do not allow uploading PHP files\n";
exit;
}
}
$uploaddir = 'uploads/';
$uploadfile = $uploaddir . basename($_FILES['userfile']['name']);
if (move_uploaded_file($_FILES['userfile']['tmp_name'], $uploadfile)) {
echo "File is valid, and was successfully uploaded.\n";
} else {
echo "File uploading failed.\n";
}
?> The expression preg_match ("/$item\$/i", $_FILES['userfile']['name']) matches the file name specified by the user in the blacklist array. The "i" modifier makes our expression case-insensitive. If the file extension matches any of the entries in the blacklist, the file will not be uploaded.
If we try to upload a file with the extension .php, this will result in an error.
These are two very simple examples, but they show possible options. It is very important how the problem is described in the project specifications, if there is one.
I'd like to talk a little about filters and streams, which were introduced in PHP 4.3 and provided scripts with an abstract layer for accessing files.
Various resources in PHP (network connections, compression protocols, etc.) can be thought of as data "streams." You can sequentially read information from or write to these streams. There are also a number of filters registered in PHP that can be used to modify the data retrieved from a stream. To get a list of the filters available in your system, simply run the following code:
print_r(stream_get_filters()); To use a filter, it must be associated with a stream. This is done using the stream_filter_append/stream_filter_prepend function or the php://filter wrapper. The former method provides more flexibility in working with filters, but the latter is more compact, which also offers certain advantages.
In general, PHP allows you to substitute one wrapper for another, which greatly reduces code. For example, connecting to a remote FTP server, downloading a gz archive from it, unpacking that archive, and saving it on your web server can be coded in just one line:
copy('compress.zlib://ftp://user:pass@ftphost.com:21/path/file.dat.gz', '/local/copy/of/file.dat'); The php://filter wrapper is also used to ensure the security of web applications. For example, the script:
include ($_POST['inc']); Setting "allowurlinclude = Off" will prevent an attacker from performing an RFI attack. However, this script does allow local PHP files to be read—all you need to do is send the following POST request to the vulnerable script: inc=php://filter/read%3Dconvert.base64-encode/resource%3D/path/script.php
While built-in filters offer impressive capabilities for solving a wide variety of problems, PHP developers have gone further and allowed web developers to create their own filters. And that's where things get interesting.
FILE UPLOAD VULNERABILITIES
The first vulnerability I'd like to discuss is insufficient filename handling during uploads. This vulnerability is marked as private on bugs.php.net, but if you dig around, you can still find a description.
The bug is that if a filename begins with a slash or backslash and contains no further slashes or backslashes, it is passed as is to the $_FILES[uploadfile][name] array element. This means that instead of uploading the file to the script's current directory, we'll upload it to the web server's root directory.
On Unix-based machines, we won't be able to upload anything to the root directory due to insufficient permissions. However, on Windows machines, this trick is quite feasible.
The second vulnerability is more significant.
It's caused by improper handling of keys in the $_FILES array. I first learned about it from someone named Qwazar on the rdot.org forum. Together with BlackFan, another forum member, they conducted tests that uncovered the nature of this bug.
With their permission, I'll describe it in more detail. So, let's assume we have a multi-file upload, implemented using the copy function:
foreach ($_FILES["file"]["tmp_name"] as $key => $name)
{
echo "Size:".$_FILES["file"]["size"][$key]."<br/>\r\n";
echo "tmp name:".
$_FILES["file"]["tmp_name"][$key]."<br/>\r\n";
if($_FILES["file"]["size"][$key]>0 &&
$_FILES["file"]["size"][$key]<1024)
{
echo "Ok<br/>\r\n";
copy($_FILES["file"]["tmp_name"][$key],'test.txt');
}
} This allows us not only to upload files, but also to read arbitrary content from the server! If we send files to the server using a form like this:
<form action="upload.php" method="POST"
enctype="multipart/form-data">
<input type="Hidden" name="MAX_FILE_SIZE"
value="10000000">
<input type="file" name="file[tmp_name][">
<input type="file" name="file[size][">
<input type="file" name="file[name][">
<input type="submit" value="submit">
</form> then elements of the following type are created in the $_FILES array:
$_FILES["file"]["tmp_name"]["[name"] The copy function handles these elements quite well:
$_FILES["file"]["tmp_name"][$key] This way, we get the ability to manipulate arbitrary parameters in $_FILES.
If the above script (let's call it upload.php) is present on the remote server, and we have a corresponding HTML form on our computer, then to read the source code of the secret.php script, which is located in the same directory as upload.php, we need to create two files on our hard drive: 1. A file named secret.php, the contents of which are not so important (let's say, for example, it's "<?php ?>"). 2. A file with a very simple name, say "1." Its contents will consist of a single character, "1."
The second file's name is a number so that it can pass the following check:
$_FILES["file"]["size"][$key]>0 Now open the above form in your browser and select secret.php in the "file[tmp_name][" field, and the file named "1" in the other fields. Then click submit, and you'll see that test.txt has appeared in the same directory. It's an exact copy of secret.php, but has a .txt extension, meaning we can easily view it in the browser.
By the way, to view a file from any directory, you need to change the Content-Type field (the one I mentioned above). In this field, we can specify the path to any file on the server, and that file will be successfully copied to test.txt. But that's not all.
"SAFE" FILE UPLOADING TO THE SERVER
As noted above, file uploading is primarily accomplished using the moveuploadedfile and copy functions. However, there are other options for accomplishing this complex and demanding task. One such option (which, by the way, is preferable if we're only uploading images) is to use the imagecreatefrom/image functions. Since these functions only work with images, we can't feed them anything other than an image. For example, a script:
$img = imagecreatefromjpeg($_FILES["filename"]["tmp_name"]);
imagejpeg($img, "uploads/".$_FILES["filename"]["name"]); Uploads only the JPEG image to the server, completely destroying all data contained in EXIF tags. This prevents an attacker from uploading anything dangerous to the server. But even this seemingly foolproof method has its pitfalls.
So, the key feature of the imagecreatefrom functions is that they not only work with image files but also fully support the streams described above! This opens up, for example, the excellent opportunity to store images not on the server, but in a database. So, if you run an image through base64_encode and save it to the database, you can later display it on the screen, for example, like this:
$jpegimage = imagecreatefromjpeg(
"data://image/jpeg;base64," . base64_encode(
$sql_result_array['imagedata']));
imagejpeg($jpegimage); This feature can be quite useful, as uploading images to a database is much safer than uploading them to files. For example, developers don't have to worry about access rights to image directories, whether these directories are accessible from the web, or about other similar issues. However, the fact that the functions accept streams occasionally leads to rather unexpected results.
Let's assume we have a web application that has the vulnerable filter described above and also performs multi-file uploads, not using the copy function, but using the imagecreatefrom/image function, like this:
foreach ($_FILES["file"]["tmp_name"] as $key => $name) {
echo "Size:".$_FILES["file"]["size"][$key]."<br/>\r\n";
echo "tmp name:".$_FILES["file"]["tmp_name"][$key]."<br/>\r\n";
$img = imagecreatefromjpeg(
$_FILES["file"]["tmp_name"][$key]);
imagejpeg($img, './new_'.$key.'.jpg');
ImageDestroy($img);
} We create a file 1.jpg on the server with arbitrary content, select it in all the fields of the form I provided above, and send a POST request with a modified Content-Type field:
php://filter/read%3dconvert.lightning_template_filter/
resource%3d
data://text/plain%3bbase64,eyUgaWYgcHJpbnRfcihpbmlfZ2V0X2FsbCgpKSAlfQ Thus, we can execute arbitrary code on the server.
The curious thing about this example is that the entry point is the seemingly harmless imagecreatefromjpeg function.
However, it's worth noting that the ability to execute arbitrary code is only possible through a vulnerable filter, and such filters are not common.
To set up secure file uploads in PHP, follow these best practices:
Validate the file type – Always check the MIME type and file extension using mime_content_type() or finfo_file() to ensure only allowed formats (e.g., images, PDFs) are uploaded.
Limit file size – Set a maximum file size using MAX_FILE_SIZE in HTML and ini_set('upload_max_filesize') in PHP to prevent large uploads.
Rename uploaded files – Generate unique filenames with functions like uniqid() to avoid overwriting existing files or exposing server paths.
Store files outside the web root – Save uploaded files in a non-public directory and serve them via a controlled script.
Check for malicious content – Use antivirus scanning tools or validate image headers (e.g., getimagesize()) to prevent file injection attacks.
Use proper permissions – Set folder permissions to 644 or 600 to restrict unauthorized access.
Note: Always disable direct script execution in upload folders by adding an .htaccess file with php_flag engine off.
These steps ensure your PHP file upload system stays secure and protected from common vulnerabilities.
We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.