Handling File Uploads with PHP

So you want to add a file uploader to your site. It’s quite easy to do with PHP, but first you must understand the inherent risks. You’re going to allow just anyone to take a file and put it on your server. That file could be anything. It could be an image like you may intend, or someone could get clever and try to upload a malicious PHP script, which could then be run when called by the appropriate URL. Or a user could upload larger files than you intended and waste your server’s storage space. (This is assuming you intend to have a public-facing uploader, of course. It’s less of an issue if its a back-end feature.)

Let’s start with the basics of setting up the form, and handling the uploaded file. Then we can tackle some of the security issues.

For the upload to work, you must add enctype="multipart/form-data" to your form tag. This signals that the POST request will contain upload data as well as the form field values.

Among fields you’ll need are a hidden field named MAX_FILE_SIZE, which tells the client not to accept a file over a certain number of bytes (300000, or 300 kilobytes, in this example) as well as the file upload field itself.

<form method="post" enctype="multipart/form-data">
	<input type="hidden" name="MAX_FILE_SIZE" value="300000" />
	<input type="file" id="myupload" name="myupload" />
	<input type="submit" name="submit" value="Submit" />
</form>

This form is going to submit to itself (that is, the PHP file that outputs the form is also the script that processes the data) so it is unnecessary to specify an action value for the form.

The code that processes the upload is actually just a couple lines. The rest is error-checking.

if ($_POST['submit']) { //If the form was submitted, commence doing stuff

	if ($_FILES['myupload']['error'] != 0) {
		//The upload failed for some reason, so output a human-friendly error message for the corresponding error number.
		$errcode = array(
			"No errors",
			"File exceeded the PHP INI upload_max_filesize value.",
			"File exceeded the maximum allowed size.",
			"Partial upload.",
			"No file uploaded.",
			"UPLOAD_ERR_NO_TMP_DIR",
			"UPLOAD_ERR_CANT_WRITE",
			"UPLOAD_ERR_EXTENSION",
			"UPLOAD_ERR_EMPTY"
		);
		$error = $errcode[$_FILES['myupload']['error']];
		echo "Error: " . $error;
	}
	else {
		//No errors, so we can move the uploaded file to our uploads directory
		move_uploaded_file($_FILES['myupload']['tmp_name'], "./uploads/".$_FILES['myupload']['name']);
		unset($_FILES);
	}

}

The most important part here is the move_uploaded_file() function, which does what it says on the box. The first argument is the temporary path of the uploaded file (which $_FILES[‘myupload’][‘tmp_name’] contains) and the second is the destination. In the example, I set it to use the name of the uploaded file (you may want to rename it) and put it in ./uploads.

That should be enough for a basic file uploader, but it does absolutely nothing to check that the uploaded data is what you’re expecting. It could be exploited terribly easily.

To combat abuse, you should add some checks. A couple of good things to look for are:

  • The MIME type. Inspect the uploaded file and make sure it’s an image file (or whatever kind of document you’re wanting).
  • The existence of “.php” in the filename.

It’s also a good idea for you to set the permissions of your uploads folder to disallow execution of shell scripts.

Checking the MIME type of an image is surprisingly easy, since there’s a handy function built-in to retrieve that information from a valid image file.

$imgdata = getimagesize($_FILES['myupload']['tmp_name']);
if (!in_array($imgdata['mime'], array( 'image/gif', 'image/png', 'image/jpeg', 'image/pjpeg' ))) {
	//This isn't an image file. Better display an error and NOT move the image to its permanent spot. The temp file will be deleted automatically.
}

Preventing PHP files from being uploaded, fortunately, doesn’t require a scary Regular Expression. You can just use strpos() to check for the presence of a substring.

if (strpos(strtolower($_FILES['ad_img']['name']), '.php') !== false) {
	//Stop right there. Why does an image file have '.php' in it?
}

That’s enough to discourage your average script kiddie, though someone with real skill might be able to find a way to get around such checks. If anybody has something to add, I’d like to hear it.

  • http://sanisoft.com/blog Tarique Sani
  • http://unsecuredbusinesscapital.com Florence

    Hmm is anyone else encountering problems with the pictures on
    this blog loading? I’m trying to figure out if its a problem on my end or if it’s the
    blog. Any feedback would be greatly appreciated.

  • http://pcexperts.edublogs.org Ivey

    Good day! Do you use Twitter? I’d like to follow you if that would be ok. I’m definitely enjoying
    your blog and look forward to new updates.