Introduction
Does your PHP website need to accept uploads? Do you maybe let customers upload graphics, or maybe PDFs? If you do, I have good news, and bad news.
First, the bad news: Accepting uploads is stunningly dangerous. There might be rogue code lurking in that JPG that could compromise your WordPress site (like CVE-2014-1905). There might be exploit code hiding in the PDF (like in CVE-2013-0724) you just let someone upload. The danger might be obvious in that it could take down your whole site. Or it might be subtle and only steal cookie information from your customers. But there’s no doubt about it: accepting file uploads is dangerous for your site, and it’s dangerous for your customers.
Now the good news: It’s not hard to put reasonable protections in place. It takes a little extra work, but that extra work is nothing compared to the amount of effort needed to clean up after a compromise. And that little extra work up front won’t drive your customers away. Far from it! Every day without a compromise is a day of added trust in your customer accounts!
There are four easy things you can do to protect yourself from malicious uploads.
The Four Easy Things
The first thing you should do is enforce the extension that you expect. If you’re allowing your customers to upload JPG files, your code should enforce that extension. If you accept PDF files, you should enforce PDF. The reason is a little embarrassing, at least from the perspective of the server: the wrong extension can confuse the web server to the point it might execute arbitrary code. An example of that is CVE-2014-1905, mentioned above.
I tested that vulnerability, and I was convinced it was impossible! I have to admit I was surprised when I entered simple PHP commands like into a JPG’s comments section using Gimp:
<?php echo phpinfo(); ?>
By giving the file a nonsensical extension like .php.blasdfa, I tricked Apache into treating the file as PHP. It displayed the output of phpinfo(). If I’d been a malicious actor, I could have inflicted untold damage to myself!
You can check the extension by looking at the $_FILES array. If the uploaded file was in an HTML field named file2scrub, you could check its name like this:
$strOriginalFileName = $_FILES["file2scrub"]["name”];
If you’d prefer not to check the name and just substitute the name you know you need (like “.jpg”), you could do something like this:
$strBaseName = basename($_FILES["file2scrub"]["name”]); $strNewName = $strBaseName . “.jpg”;
The second way to protect yourself is to limit the size of the upload to the maximum a reasonable file should be. This might be hard because the definition of “normal” can change over time. But setting a limit protects you against a malicious actor who might want to upload dozens of 4Gb ISO files to exhaust your disk space — thus giving you a crash course (pun intended!) in one of the many ways to inflict a Denial of Service (DOS) attack.
As a developer at a large company, it’s unlikely you’d be responsible for configuring this value. Your Operations team would probably take care of it. But if you’re also the Apache administrator for your PHP site, you can configure the maximum file upload size in php.ini. On Linux boxes, it’s usually located here:
/etc/php.ini
If you want to allow files up to 10Mb to be uploaded, you could change/add these values:
upload_max_filesize = 10M post_max_size = 10M
Notice post_max_size. No point in allowing 10Mb uploads if you only allow 2Mb posts!
The third way to protect yourself (and these steps are cumulative; enforce the extension and control the upload size before doing these next steps) is to perform a basic test of the file’s contents before accepting it into your application. You can do this with PHP’s finfo_open function (using the constant mime_content_type) and PHP’s finfo_file function. Here’s the first part of a sample:
// Good reference: https://www.w3schools.com/php/php_file_upload.asp $strTargetDir = "/tmp/"; // Not completely safe; name could be altered to be an attack $strTargetFile = $strTargetDir . basename($_FILES["file2scrub"]["name"]); $strTempFileName = ($_FILES["file2scrub"]["tmp_name"]); $strFirstCheck = finfo_test($strTempFileName);
Assuming that we uploaded the file in the HTML field named “file2scrub”, this code places the file in /tmp. Then, it called the custom function called “finfo_test” and acts on the result. Here’s the code for that function:
function finfo_test($strTempFileName) { if (!file_exists($strTempFileName)) { return "File does not exist."; } // See http://stackoverflow.com/questions/8444638/how-to-read-header-of-a-file-uploaded-in-php#comment-10454480 $finfo = finfo_open(FILEINFO_MIME_TYPE); $mime = finfo_file($finfo, $strTempFileName); finfo_close($finfo); return $mime; }
This code makes sure the file exists (we don’t want random failures to throw errors messages on the screen!). Then, it uses finfo_file, specifying the constant FILEINFO_MIME_TYPE, to define what kind of information we want about the file. Then, we call finfo_file to get the actual MIME type. This table shows a number of common file types and the MIME type PHP thinks it is:
Actual File Type | PHP Reports |
---|---|
Old Word with *.doc | application/msword |
PDF with *.pdf | application/pdf |
RTF with *.rtf | text/rtf |
Excel with *.xls | application/vnd.ms-office |
PowerPoint with *.pptx | application/vnd.openxmlformats-officedocument.presentationml.presentation |
Excel with *.xlsx | application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
DOCX with *.docx | application/vnd.openxmlformats-officedocument.wordprocessingml.document |
BZ2 with *.bz2 | application/x-bzip2 |
iDVD with *.dvdproj | application/x-bzip2 |
Apple Pages with *.pages | application/zip |
Scrivener with *.scriv | application/zip |
GIF with *.gif | GIF with *.gif |
JPG with *.jpg | image/jpeg |
PNG with *.png | image/png |
Calendar with *.ics | text/calendar |
CSV with *.csv | text/plain |
HTML with *.html | text/html |
Text file with *.txt | text/plain |
SQL with *.sql | text/plain |
Text file with *.docx | text/plain |
As you can see, PHP’s built-in functions go a pretty good job of identifying the type of file based on its contents.
At this point, you might be tempted to declare victory! To be perfectly honest, this isn’t a bad place to stop, but there’s one more thing you can do, at least for some file types. You could perform a very basic operation that’s as low risk as possible to increase the odds that the file’s legitimate.
That’s the fourth thing you can do: a basic functional test. I’ll give you an example based on a JPG upload. Consider this custom function:
function test_jpg($strTempFileName) { // See http://php.net/manual/en/language.operators.errorcontrol.php $imgTest = @imagecreatefromjpeg($strTempFileName); if (!$imgTest) { return "Not a JPG"; } else { return "Confirmed: JPG"; } }
The function imagecreatefromjpeg will try to load the source file and convert it to a JPG. It works even if the source is already a JPG. So without trying to display the file, or without trying to manipulate it (resize, downgrade the resolution, etc.), we get an added level of assurance that the file’s legitimate if the function succeeds. If the function fails, we know we should discard the file as unsafe.
Summary
Do you really need to worry about the file contents when you let users upload files to your site? Can’t you just wash your hands of the responsibility by displaying a disclaimer? You’d need to consult your favorite legal representative for the definitive answer, but from my perspective, there are a few things you should consider:
- Do users have to use your site? Are they compelled by law, or by market share, to use your product? If the answer’s “No,” then it’s in your best interest to not let your site turn into a hub for distributing malware to your customers.
- Do you want to delight your users? Do you want people to use your site because they trust it? Do you care about your reputation? If the answer to any/all of those is “Yes,” then I think making a little extra effort to implement these ideas (or maybe even ideas of your own!) is a good investment of time.
Have you had experience implementing any upload safety checks? How did it go for you? Let me know in the comments below!
by Terrance A. Crow
Terrance has been writing professionally since the late 1990s — yes, he’s been writing since the last century! Though he started writing about programming techniques and security for Lotus Notes Domino, he went on to write about Microsoft technologies like SQL Server, ActiveX Data Objects, and C#. He now focuses on application security for professional developers because… Well, you’ve watched the news. You know why!