[PHP] Upload Script

Will

Senior Administrator
Staff member
Joined
Mar 4, 2012
Posts
8,195
Location
%tmp%
I was trying to write a PHP Upload script, I'm using a basic script from a book as the starting point, and I was trying to make a more complicated version except I'm completely confused by an early error.

My complete code is below. This script does not work, I simply get the HTML form and "No image has been uploaded" as the output.

Code:
<?php // upload.php


//####################### Get DB Info #######################
/*require_once 'login.php';
$db_server = mysql_connect($db_hostname, $db_username, $db_password);

if (!$db_server) die("Unable to connect to MySQL: ". mysql_error());

//######################## Functions ########################

function mysql_fix_string($string)
{
	if (get_magic_quotes_gpc()) $string = stripslashes($string);
	return mysql_real_escape_string($string);
}

function mysql_entities_fix_string($string)
{
	return htmlentities(mysql_fix_string($string));
}*/



//######################## HTML Form ########################

echo <<<_END
<html><head><title>PHP Form Upload</title></head><body>
<form method='post' action='upload.php' enctype=multipart/form-data'>
Select File: <input type='file' name='filename' size='10'/>
<input type='submit' value='Upload' />
</form>
_END;

//###########################################################



if ($_FILES)
{
	echo "why does this not work?";
	/*
	$name = $_FILES['filename']['name'];

	switch($_FILES['filename']['type'])
	{
		case 'image/jpeg':	$ext = 'jpg' ; 	break;
		case 'image/gif':	$ext = 'gif'; 	break;
		case 'image/png':	$ext = 'png'; 	break;
		default:			$ext = ''; 		break;
	}
	if ($_FILES)
	{
		
		//begin mysql 
		$timestamp = time();
		$size = $_FILES['filename']['size'];
		$user = 'unregistered';
	*/

		//begin upload
		$name = $_FILES['filename']['name'];
		move_uploaded_file($_FILES['filename']['tmp_name'], $name);
		echo "Uploaded image '$name' <br /><img src='$name' />";
	//}	
	//else echo "'$name' is not an accepted image file";
}
else echo "No image has been uploaded";

echo "</body></html>";

//###########################################################

?>

Everything to do with MySQL has been commented out, everything to do with the file validation has been commented out. It's a little messy, but I was trying to get the basic upload script working before adding more complicated features - learning as I go.

Although the script above doesn't work, this one DOES:

Code:
<?php //upload_test_only.php

echo <<<_END
<html><head><title>PHP Form Upload</title></head><body>
<form method='post' action='upload_test_only.php' enctype='multipart/form-data'>
Select File: <input type='file' name='filename' size='10' />
<input type='submit' value='Upload' />
</form>
_END;

if ($_FILES)
{
	$name = $_FILES['filename']['name'];
	move_uploaded_file($_FILES['filename']['tmp_name'], $name);
	echo "Uploaded image '$name' <br /><img src='$name' />";
}

echo "</body></html>";
?>

This script works fine - as far as I can see the scripts should be almost identical (excluding the commented out code). The first script did briefly work, but as I uncommented code it stopped working again. The problem is, I've now commented out everything that's different but it's still not working. :huh:

I'm using XAMPP to test, running PHP Version 5.4.7. Any suggestions?
 
I never worked out the issue with the above script - I was in the middle of writing a more advanced version, but I wanted to known why the script didn't work.

The whole thing is part of a practice PHP Project to write a file submission webpage. Files could be submitted by users, with the download page limited to helpers - similar to how several forums have malware submission pages. Several sites use a system similar to this already, but this is just a practice project for me to get better at PHP. Essentially this is the first real PHP project I've written.

Progress so far - bulk of the upload script itself is done. I still need to add logging, error checking, and a couple of other features, but it's in a basic working state. Not yet written all the MySQL queries, and intending on adding more. The uploader would be accompanied with a CRON job probably, that would clear old uploads and check the DB status etc.

Error checking is also not written yet, because I'm new to PHP decided to do it later. :grin1:

Index.php:

Code:
<?php

echo <<<_END
<html><head><title>PHP Form Upload</title></head><body>
<form method='post' action='upload.php' enctype='multipart/form-data'>
Select File: <input type='file' name='filename' size='10' />
<input type='submit' value='Upload' />
</form>
_END;

?>

login.php:

Code:
<?php // login.php
//======================================================================
$db_hostname = 'localhost';
$db_database = 'database';
$db_username = 'username';
$db_password = 'password';


//Logging Info
$uploaddirectory	 = '/xampp/htdocs/upload/uploads/'; //where uploaded files should be saved
$logdirectory = '/xampp/htdocs/upload/uploads/'; //where logfile should be saved

$alertemail = 'example@example.com'; //email for flood alert warnings

//Restrictions

$MaxUploadSize = 3000000; // MaxUploadSize of an individual file.
$MaxStorage = 0; // MaxStorage - Storage space dedicated to uploads. Uploads will be stopped if limit is reached.

//======================================================================
?>

filehandler.php:

Code:
<?php //filehandler.php
//contains class for handling file uploads.

/*TO DO:

	- Add proper error checking.
	- Add proper logging of success and errors.
	- Rewrite file validation to work with zip and strip extension. Validate must return true/false.
	- Rewrite file upload function to take into account new validation method.
	- Validate FileName length - MAX NAME LENGTH

*/



//###########################################################

class FileHandler {

	private $savedir = '';
	private $logdir = '';

	private $allowedExtensions	= array(
		'application/zip'				=> 'zip',
		'application/x-rar-compressed'	=> 'rar', 
		'text/x-log'					=> 'log',
		'image/jpeg'					=> 'jpg'
		);

	private $maximumFileSize	= 1;

	private $systemErrors = array(
		'noUploaddir'		=> 'Upload directory does not exist',
		'noFiles'			=> 'No Files were uploaded.',
		'extension'			=> 'Only the following file types are allowed',
		'mime'				=> 'This file\'s mimetype is now allowed - please try another file.',
		'unknown'			=> 'Unknown error uploading file',
		'maximumFileSize'	=> 'exceeds the maximum file size - '
	);

	private $uploadErrors = array(
		UPLOAD_ERR_INI_SIZE		=> 'The uploaded file exceeds the upload_max_filesize directive -- (UPLOAD_ERR_INI_SIZE)',
		UPLOAD_ERR_FORM_SIZE	=> 'The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form -- (UPLOAD_ERR_FORM_SIZE)',
		UPLOAD_ERR_PARTIAL		=> 'The uploaded file was only partially uploaded -- (UPLOAD_ERR_PARTIAL)',
		UPLOAD_ERR_NO_FILE		=> 'No file was uploaded -- (UPLOAD_ERR_NO_FILE)',
		UPLOAD_ERR_NO_TMP_DIR	=> 'Missing a temporary folder -- (UPLOAD_ERR_NO_TMP_DIR)',
		UPLOAD_ERR_CANT_WRITE	=> 'Failed to write file to disk -- (UPLOAD_ERR_CANT_WRITE)',
		//UPLOAD_ERR_WRONG_TYPE 	=> 'File upload stopped by extension -- (UPLOAD_ERR_WRONG_TYPE)' //This line needs fixing
	);

	private function log($savename){
		//log successful upload


		//TO DO - write error checking first

	}


	private function validate_zip(){
		//TO DO
		return true;
	}

	private function validate_rar(){
		//TO DO
		return true;
	}

	public function checkFiles(){
		//validate file

		if ($_FILES){

			if ($this->validate_rar() or $this->validate_zip()){
				return true;
			}

			else {
				echo "Not a valid file type.";
				return false;
			}
			
		}

		else echo "_FILES does not exist"; //sort out error checking later.		
	}




	public function setAllowedExtensions(array $extensions){
		$this->allowedExtensions = $extensions;
	}


	public function setMaximumFileSize($value){
		$this->maximumFileSize = $value;
	}

	public function setSaveDir($value){
		$this->savedir = $value;
	}

	public function uploadFiles(){

		$ext = $this->checkFiles();
		$filesize = $_FILES['filename']['size'];

		if ($this->savedir && $ext && $filesize <= $this->maximumFileSize){

			$id = time();
			$savename = $this->savedir.$id;

			move_uploaded_file($_FILES['filename']['tmp_name'], $savename);
			$this->log($savename);

			echo "Upload Success!";

			return $id;

		}

		elseif (!$this->savedir) {
			echo "No Save Directory Found.";
		}

		elseif (!$ext){
			echo "Not a valid filetype.";
		}

		elseif ($filesize > $this->maximumFileSize){
			echo "Filesize is too big.";
		}

		else echo "Unknown error occured.";

	}

}

Several things in filehandler.php are no longer used - originally I was validating files using MIME type, but quickly realised this wasn't particularly secure and had too much variation over browser. My current plan is to only accept ZIP/RAR files, and examine the file data itself to verify the file type. Also not 100% secure, but I can't think of another way of doing it. As files are all being stored without file extensions in a non-public folder (or will be), I think this should be sufficient?

upload.php:

Code:
<?php // upload.php

/*
//TO DO

- Add Flood Control.
- Add MaxStorage check.
- Add Logging.
- Add Error Checking.
- Add VirusTotal DB Scan.
- Add URL from HTML Form.
- Fix MD5.

*/
//######################################### GET DB INFO ########################################
require_once 'login.php';
require_once 'filehandler.php';

//MySQL connect
$db_server = mysql_connect($db_hostname, $db_username, $db_password);
if (!$db_server) die("Unable to connect to MySQL: ". mysql_error());

//selectDB
mysql_select_db($db_database);

//########################################## FUNCTIONS #########################################

function mysql_fix_string($string)
{
	if (get_magic_quotes_gpc()) $string = stripslashes($string);
	return mysql_real_escape_string($string);
}

function mysql_entities_fix_string($string)
{
	return htmlentities(mysql_fix_string($string));
}

function check_flood()
{
	//NOT YET IN USE
	return true;
}

function get_ip()
{
	return $_SERVER['REMOTE_ADDR'];
}

function get_real_ip()
{
	//NOT YET IN USE
}

//######################################## UPLOAD FILES ########################################

$flood_allow = check_flood(); //Check for Flood.

if ($_FILES && $flood_allow){

	$fh = new FileHandler();
	$filename = $_FILES['filename']['name'];
	$tmpname = $_FILES['filename']['tmp_name'];
	$validate = $fh->checkFiles();

	if ($validate){

		//set upload settings
		$fh->setSaveDir($uploaddirectory);
		$fh->setMaximumFileSize($MaxUploadSize);

		//begin upload
		$id = $fh->uploadFiles();

		//get info
		$timestamp 	= mysql_entities_fix_string(time());
		$size 		= mysql_entities_fix_string($_FILES['filename']['size']);
		$user 		= mysql_entities_fix_string('unregistered');
		$md5  		= mysql_entities_fix_string('12354adfkj23445adfkj');   //md5_file($filename);
		$url  		= mysql_entities_fix_string('www.example.com');
		$userip 	= mysql_entities_fix_string(get_ip());

		//prepare query
		$query = 'PREPARE statement FROM "INSERT INTO files
					VALUES(?,?,?,?,?,?,?,?)"';

		$result = mysql_query($query);
		if (!$result) die ("Database access failed: ". mysql_error());
		$result = NULL;

		//set placeholders
		$query = 	'SET @id = "'.$id.'",'.
					'@filename = "'.$filename.'",'.
					'@size = "'.$size.'",'.
					'@date = "'.$timestamp.'",'.
					'@md5 = "'.$md5.'",'.
					'@url = "'.$url.'",'.
					'@username = "'.$user.'",'.
					'@userip = "'.$userip.'"';

		//submit query and check success
		$result = mysql_query($query);
		if (!$result){
			die ("Database access failed: ". mysql_error());
			$uploadsuccess = fail;
		}
		$result = NULL;

		//execute submission
		$query = 'EXECUTE statement USING @id,@filename,@size,@date,@md5,@url,@username,@userip';

		//submit query and check success
		$result = mysql_query($query);
		if (!$result){
			die ("Database access failed: ". mysql_error());
			$uploadsuccess = fail;
		}
		$result = NULL;

		################################# UPDATE DBSTATUS ####################################

/*
		//get database total_size
		$query = //WHERE db_name = $db_database .... something... SELECT total_size ??

		//submit query

		$result = mysql_query($query);
		if (!$result) die ("Database access failed: ". mysql_error());
		$result = NULL;

		//calculate new total_size
		$total_size = $result + $size; //check this. Can mysql_entities_fix_string() break this?

		//update database total_size
		$query = //WHERE db_name = $db_database .... ALTER total_size '$total_size' ??

		//submit query
		$result = mysql_query($query);
		if (!$result) die ("Database access failed: ". mysql_error());
		$result = NULL; 

*/

		#######################################################################################

	}

}

elseif (!$flood_allow){
	echo "You have reached the upload limit. Please wait before uploading.";
}

elseif (!$_FILES){
	echo "No file has been selected.";
}

else{
	echo "Unknown error has occured.";
}

//##################################### Virus Total Upload #####################################

if ($uploadsuccess){

	// TO DO
}




//############################################# LOG ##############################################




//################################################################################################

mysql_close($db_server);

?>

--------------------------------------------------------------------------------------

I've tried to make the code as secure as I can, adding any decent security features I can think of. Couple of questions though.

1) The intention is to store uploaded files, renamed as a timestamp, outside the public_html folder and without any extension. Is this safe? Is there a more secure way of storing uploaded files?

2) How can I improve the structure of this program? It's currently in a working state, but just feels very messy.

3) I'm not sure if I'm making the best use of the MySQL databases. I currently have:

Code:
Table1 - files:

id 		filename 	size 	date 	md5 	url 	username 	userip 	comments

Table2 - dbstatus

Code:
total_size	total_uploads 	current_uploads 	last_cleared 	most_active_real_ip 	most_active_proxy_ip 	most_active_user

I'm also intending to store VirusTotal results in the database - these would then be displayed to helpers before downloading. VirusTotal returns a JSON Array of 48 virus scan results - would it be best to create a new column for each different scanner? (Making in total approx 50 columns for each scanned file) Is there a better way of storing these results?

Overall - is the MySQL okay? The dbstatus table will only have one entry, but the columns for the entry will be updated regularly. Is there a better way of doing this?

4) Any more security I could add to this? This is only a practice project, but I'd like it to be secure enough to feasibly add to a real website without adding additional security risks.
 
I don't have dreamweaver installed on my computer yet which is why I haven't commented here.

When you upload a file it is generally uploaded to the temp directory specified by PHP. By default this is usually above the public_html folder and therefore inaccessible by normal means.

What I generally do from there is convert it to base10 if it needs to be and store it in a SQL table as a BLOB.

To get the file back down in a usefull manner you need another script that will pull it down and convert it back to whatever it needs to be in.

I've done this with many data types however, it does lead to interesting URLs generally something like:

whatever.php?file=file1

Or something similar. You could easily fix this with some masking... (.htaccess)
 
I'd considered storing files in the SQL table already - read a variety of things about whether that's a good idea. I know that vBulletin stores attachments in MySQL like that, but I decided against doing it at the moment. I might code it both ways just to see how it would work though.

Is storing files without extension on a Linux server safe? At least, if they're outside of public_html. Is there a better way to do that?
 
You can store it outside of public_html and the extensions really won't matter however just keep in mind this for the most part disables hotlinking unless you manually create each download page.

Not sure about removing the extension...
 

Has Sysnative Forums helped you? Please consider donating to help us support the site!

Back
Top