PHP: Running *.jpg as *.php or How to Prevent Execution of User Uploaded Files

Level: starter

Image for post
Image for post

Every professional PHP developer knows that files uploaded by users are extremely dangerous. They can be used by attacker at backend as well as at frontend.

I decided to write an article because about a month ago I visited a thread at reddit called PHP upload vulnerability check. User darpernter asked a tricky question:

Does the attacker still possible to launch his php file eventhough I have renamed it to ‘helloworld.txt’?

The first answer were:

If it’s renamed to .txt then it won’t run the PHP, so you should be fine, but double check to make sure it’s not uploaded as .php.txt

Unfortunately, that is not the right answer for that question. I don’t want to say it’s completely wrong. But it certainly incomplete. Surprisingly, all other answers were very similar.

I tried to clarify the problem. My comment turned out to be pretty big, and I decided to make it even bigger.

Problem

Someone allows users to upload files and is afraid that users’ files could be executed on the server.

Let’s start with how do php-files usually executed. Assume that we have a properly working web-server with PHP support, there are two common ways to execute php-file from the outside. The first one is to request directly that file with an URL like http://example.com/somefile.php. The second one — this way PHP works today — route all requests to index.php and allow it include required files somehow. So, there are two options to run a PHP code from a file: run that file or include/include_once/require/require_once into another running file.

There is a third option: an eval() function. It executes php-code from a passed string. That function is used mostly in various CMS to run the code stored in the database. eval() function usage is very dangerous, but usually if you use it — you already know that you’re doing something dangerous and (once again) usually you just have no other choice. Indeed, eval() has it’s purpose and is very useful in some cases. But I would recommend, if you’re a newbie in PHP — avoid using it. Just look at this article at OWASP. I typed too many words starting with U, let’s get back to words starting with F.

So, if there are two ways to run a code stored in the file — run it directly or include into the file which is running — how this issue can be prevented?

Solution?

How we can understand that a file contains some PHP code? We look at the extension. If it ends with .php like somefile.php we expect it to have a php-code inside.

As I mentioned above, if I have a file somefile.php in the server root directory and typed into the browser an URL http://example.com/somefile.php it will be executed and I’ll see its output.

But what if I’ll rename this file? If I’ll rename it to somefile.txt or even somefile.jpg? What will I get then? I’ll get it’s content. It will not be executed. It will be sent “as is” right from the disk (or cache).

At that point answers from the reddit are correct. Renaming prevents automatic execution of single file, so what’s wrong?

I believe you took a notice of that question mark after “Solution”. That’s because it have to be there. Today there are very few standalone php-files in the URLs in the internet. And most of them are fake, just four characters .php in the URL kept for a backward compatibility with old URL.

Today vast majority of PHP scripts are included into others in the runtime, because all requests are sent to some index.php in the root of the site. That file includes other php-files according some logic. And that logic may be (and will be) used against you. If logic of your app allows to include/require user files — app is vulnerable and you should immediately take some measures to prevent user files been executed.

How to prevent including of user’s files?

Will renaming of file save the day? — No, it won’t!

PHP doesn’t care about the file extension. Honestly, no other program does. Yes, files are open in the corresponding programs when you double click them. But file extension is just a way to help operation system to decide which one program to use. Any program can open any file, if it has a read access to it. Sometimes program refuses to show a result of opening and processing of a file. That’s not because of its extension, but only because of its data.

Web-server is usually configured to execute a .php file and reply with output of that operation. If you request some image like .jpg — it is sent raw as it is saved on the hard drive. But if you’ll ask the web-server to somehow run a jpeg image, what will happen then? Will it consume it or not?

Image for post
Image for post

Programs do not care about file names. They do not even care whether the file has a name, or finally is it a file or something else!

What is required to run a PHP code from a file?

PHP will run code from a file in at least two cases:

  1. code between <?php and ?> tags
  2. code is between <?= and ?> tags

Code between this tags will be executed even if the file is filled with some strange binary data or have some magical protective name.

Here I have one picture for you:

Image for post
Image for post

It’s pure now. But may be you know that JPEG format allows to put some annotation into the file. Like, a camera model or location, where the photo was taken. What if we put some PHP code there and will try to include or require that picture into the file with PHP code? Let’s find out!

Problem!!1

Take that image and save it on your hard drive. Or you can take any other JPEG. You can use any file of any type. I propose to use a JPEG image because it is an image and at same time it is easy to edit the text inside. I use Windows laptop, and I have no Apple or Linux (or UNIX, yeah) laptop near at the moment. So I’ll post screenshots of that OS. But I’m pretty sure you can do the same anyway.

Create a file with this PHP code:

<h1>Problem?</h1>
<img src="troll-face.jpg">
<?php
include "./troll-face.jpg";
  1. Save a picture and name it troll-face.jpg
  2. Put the picture and the php-script into the same folder
  3. Open your browser and request the php-file

If you named your php-file index.php then just put it into the root of your file or (sometimes) it’s OK to put in any folder located in the folder of your site.

If you did everything right you’ll see that:

Image for post
Image for post

It’s OK. Not a problem. No PHP code is there, so no PHP code was executed.

Now, do add a problem:

  1. Open the file properties dialog or run some app that allows editing of EXIF info
  2. Switch to Details tab or edit that info some other way
  3. Scroll down to camera params
  4. Copy and past the following code to the “camera maker” field:
<?php echo "<h2>Yep, a problem!</h2>"; phpinfo(); ?>
Image for post
Image for post

Refresh the page!

Image for post
Image for post

Yes, you see that image on the page. And the same image was included into the PHP code of that page. And the code from the image is executed.

What should we do?!!1

Long story short: it will not be executed, if you will not create a possibility to execute it

Long story below. Read it carefully.

The Answer?

If someone sees I’m wrong somewhere — please correct me, it’s important question.

PHP is a scripting language. You always have to include some files, which path was composed dynamically. So, to protect sever, you have to check path and prevent mixing of your site files and user’s uploaded or created files. If user’s files are separated from your app files, you can check path of file before including it. If it is located in the right folder with your app scripts — then it can be executed. If not — just do not include or include_once or require or require_once this file.

How this check could be done? This is simple operation. You just need to compare $folder path to a folder, from where files are allowed to be included to path of a file $file you want to include.

// bad example, do not use!
if (substr($file, 0, strlen($folder)) === $folder) {
include $file;
}

If $folder is /path/to/folder and $file is /path/to/folder/and/file, then path to a folder will equal a string, extracted with substr() function. If file is located in different folder — this strings will not be equal.

The code above have two major problems. If file path is /path/to/folderABC/and/file, then strings again will beequal even if it is obvious, that file is not located in the right folder. This can be prevented by adding slash to both paths. It doesn't matter that we add slash to a file path here, because we just need to compare two strings.

Example: If folder path is /path/to/folder and file path is /path/to/folder/and/file, then string extracted from the file path will have the same number of characters like $folder string and will be /path/to/folder.

If folder path is /path/to/folder and file path is /path/to/folderABC/and/file, then string extracted from the file path will have the same number of characters like $folder string and will be /path/to/folder again, and it is wrong, unwanted behavior.

So, after adding slashes /path/to/folder/ when compared to extracted part of /path/to/folder/and/file returns /path/to/folder/ and it is fine.

And if /path/to/folder/ is compared to extracted part of /path/to/folderABC/and/file returns /path/to/folderA and check will fail.

This is what we want to be happy. But there is another problem. It’s not obvious. I’m sure, that if I’ll ask you, do you see here a catastrophic vulnerability — you will not guess where is it. You’ve already used that thing in your life, maybe even today. Now you’ll see how vulnerability could be in the same time hidden and obvious. Behold.

/../

Imagine a common situation.

There is a site. Users can upload their files to that site. All files are located in the special folder. There is a script, it includes a user’s file. Script makes the check from above, and if script takes a path to include based on the user’s input (directly or not) — this script can be tricked with a path like this:

/path/to/folder/../../../../../../../another/path/from/root/

Example. User makes some request, and your script includes a file, which path is based on some user input like:

include $folder . "/" . $_GET['some']; // or $_POST, or whatever

you’r in trouble. One day user will send ../../../../../../etc/.passwd or any other — he will get what he wants.

Once again. If somebody somehow managed to command your script to load a file with that name — you have a problem. It doesn’t have to be only users’ files. It could be some plugins for your CMS or your own files (trust no one), or even a mistake in the logic of the app, etc.

Or

User can upload file with a name file.php, you will put it into a special folder with user files:

move_uploaded_file($filename, $folder . '/' . $filename);

User files are stored in it, you must always check and never include files from that folder, and everything will look like it is OK. Usually, all the names of files, sent to you by your users will not have slashes or other symbols, restricted by the file system. That happens because browser sends you a filename, created in real file system, and it is a name of some exact real file. Mostly.

But http-requests allow user to send any strings. So if someone will forge request and will make filename to be a string like this one ../../../../../../var/www/yoursite.com/index.php— this code from above will override your index.php, if it is located there.

All newbies try to solve this problem by filtering filenames from ‘..’ or slashes, but it’s wrong, while you’r not experienced in security. And you must (yes, MUST) understand a simple thing: you will never be enough experienced in questions of protection or cryptography. That means, if you know about “double-dots and slashes”, that doesn’t mean you know all other vulnerabilities, attacks and even special characters, potentially transcoded from one codepage to another while file name is written in file system or database.

Solution and Answer

To resolve the problem, some special functions were embedded in PHP just to be used in that cases.

basename()

The first one — basename() It extracts part of a path, from it’s end, until it meets first slash, but ignores slash at the end of string, see examples. Anyway, you will receive a good filename. If it was already good — it will stay good. If it was enchanted with dark magic — it will become harmless.

realpath()

Another one — realpath() It converts paths to straight good real path, starting from the root, and containing no magic at all. It even will convert symlinks to a path where this symlink is pointing to.

So you can use this two functions to check paths. To check that exactly this file path really belongs to exactly this folder path.

My code

I wrote a function to perform such a check. I’m not an expert, so one can use this function on he’s or she’s own risk. Here it is:

In the upshot.

  • You have to filter user input, filename is user input. So you have to check file name. Use basename() on it. Always.
  • You have to check path, where you want to put user files, and never mix it with your app folders. File path has to be formed from your string path of a folder and a basename($filename). Resulting file path have to be checked before file is written.
  • You have to check path of file you want to include before you include it, and check it severely.
  • You have to use special functions, because you can be not enough informed about some vulnerabilities or exploits.
  • And obviously it’s not about file extension of mime-type. It doesn’t matters. JPEG allows strings inside files, so it can be valid JPEG and in the same time contain valid PHP script inside.

Don’t trust user. Don’t trust browser. Build your backend like everyone is sending you viruses.

Don’t be scared, it’s simpler, than it looks like. Just remember “don’t trust user” and “there are some functions for that”.

And may the Force be with you

The end

Written by

Founder/CEO GuildWalls.com

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store