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.