Interstell, Inc.

Security, Application Security, and Software Research

4 Ways to Protect Your PHP Website from File Uploads

Don’t panic if your PHP app needs to allow uploads. Use these 4 tips to protect your application from harmful files!

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 TypePHP Reports
Old Word with *.docapplication/msword
PDF with *.pdfapplication/pdf
RTF with *.rtftext/rtf
Excel with *.xlsapplication/vnd.ms-office
PowerPoint with *.pptxapplication/vnd.openxmlformats-officedocument.presentationml.presentation
Excel with *.xlsxapplication/vnd.openxmlformats-officedocument.spreadsheetml.sheet
DOCX with *.docxapplication/vnd.openxmlformats-officedocument.wordprocessingml.document
BZ2 with *.bz2application/x-bzip2
iDVD with *.dvdprojapplication/x-bzip2
Apple Pages with *.pagesapplication/zip
Scrivener with *.scrivapplication/zip
GIF with *.gifGIF with *.gif
JPG with *.jpgimage/jpeg
PNG with *.pngimage/png
Calendar with *.icstext/calendar
CSV with *.csvtext/plain
HTML with *.htmltext/html
Text file with *.txttext/plain
SQL with *.sqltext/plain
Text file with *.docxtext/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:

  1. 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.
  2. 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! 

Categories: Application Security, PHP

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.