Form on php with secure uploading of files to the server


13-08-2018
Денис Л.
Php
Form on php with secure uploading of files to the server

I recently made a feedback form with the ability to upload files. At the same time, the task was to make the form perfectly safe, with a "white list" of files, and also at the output to form a letter to the client department with the client's data and all its files. Since files (according to the TK) can be large (up to 50 MB each), no mail client will survive such a volume of information, especially given that the mailbox sizes of managers are always full and they can not miss the request from the client.

I decided to create a url for the ftp connection in the browser, with the password turned on. For this purpose, I created a separate user on the VPS and limited it to access only one folder, in which there will be documents. Thus, third-party users can not view these files in any way, and the managers of the client department have only to view, and only within the same folder.

All the basic settings I put into the beginning of the script.


The original script you can download via bitbucket: https://bitbucket.org/lisogorsky/dev/src/master/uploadsFilesForm/

So, how to create a secure form on php to upload files to the server


The first step is to configure php.ini at /etc/php.ini

If we do not do this, then the size of the received files can be severely limited. For example, on our VPS there was a limit of 2 MB.

post_max_size set 50М

upload_max_filesize set less, 49М

parameter memory_limit must be greater than post_max_size

For the input field with the phone, we can connect Jquery maskedinput

Create a folder in the parent directory, for example, 'clients_documents' (its name is then written in the script below)


<? header('Content-Type: text/html; charset=utf-8');
/* *** BASIC SETTINGS *** */

// here we enter all the file extensions allowed for download:
$goodFiles = ['.doc','.docx','.pdf','.xls','.xlsx','.ppt','.pptx','.zip','.rar','.7z','.jpg','.jpeg','.jpe','.bmp','.png','.txt','.rtf','.gif','.tif','.tiff','.xps','.odt','.ods','.odp','.csv'];

// here we indicate which administrator's mail should receive a letter about the new client application:
$to = "*******@*******.ru";

// here we specify from what mail should the letter come:
$from = "mail@*******.ru";

// here we enter the name of the folder in the root of the site, which we previously created for loading and storing client documents:
$uploaddir = 'clients_documents';

// specify FTP settings. They are needed for the formation of a link on which it will be possible to download client documents from the browser
// For security reasons, I recommend creating a separate ftp account, with access only to the folder with the documents.
// Those. users will be able to see only the folder with the incoming documents + folder to the level above (where there will be sent documents for other dates)
// Because script we will use only within your organization and the documents will come to your e-mail (for example, the head of the customer service department)
// then this strategy is completely safe.
$ftpHost = '81.177.165.34'; // indicate the real FTP host
$ftpLogin = 'login'; // specify the real FTP login
$ftpPass = 'password'; // specify real FTP password

// Does the user have standard FTP access (to all folders on the server) or did you configure the client to access only the directory with the documents? If option 1 is set to 'true'. If access is restricted only by the current folder - leave 'false'
$ftpAccess = false;

// set the variables obtained from $_POST. For security, we cut all the tags that a user can enter
if($_SERVER['REQUEST_METHOD'] === 'POST') {
	$today     = date('d-m-Y_H-i-s');
	$problem   = strip_tags(trim($_POST["problem"]));
	$object    = strip_tags(trim($_POST["object"]));
	$documents = strip_tags(trim($_POST["documents"]));
	$dop_info  = strip_tags(trim($_POST["dop_info"]));
	$questions = strip_tags(trim($_POST["questions"]));
	$name      = strip_tags(trim($_POST["name"]));
	$tel       = strip_tags($_POST["tel"]);
	$email 	   = strip_tags($_POST["email"]);
	$ooo       = strip_tags(trim($_POST["ooo"]));
	$files     = $_FILES;
	$subject   = "You have a new request for a callback from the site ****.ru"; // here we indicate the subject of the letter we need
	$message   = "<h3>Application with extended feedback form</h3><hr>"."\n"; // here we indicate the title of the letter

	// All fields below are filled from a specific example of the form below. If you have other fields, we change the name of the variables below
	// the values of the variables in $_POST['...'] must match the values of the attribute 'name' in the input fields (! this is important!)
	if(!empty($_POST["problem"])) $message .= "<b>The problem and purpose of involving an expert organization:</b><br>{$problem}<hr>"."\n";
	if(!empty($_POST["object"]))  $message .= "<b>Object of examination:</b><br>{$object}<hr>"."\n";
	if(!empty($_POST["documents"])) $message .= "<b>Available documentation for the facility:</b><br>{$documents}<hr>"."\n";
	if(!empty($_POST["dop_info"]))  $message .= "<b>Additional Information:</b><br>{$dop_info}<hr>"."\n";
	if(!empty($_POST["questions"])) $message .= "<b>Tasks (questions) for experts:</b><br>{$questions}<hr>"."\n";
	if(!empty($_POST["ooo"])) $message .= "<b>Name of the organization:</b><br>{$ooo}<hr>"."\n";
	
	$message .= "<b>The contact person:</b><br>{$name}<hr>"."\n"."<b>Телефон:</b><br>{$tel}<hr>"."\n"."<b>E-mail:</b><br>{$email}<hr>"."\n";

	$extensionArr = []; $filesName = [];

	// Below is the path to the folder we specified above in the variable $uploaddir
	// in this example, the location folder of this script and the folder 'clients_documents' are at the same level, in the root of the site, i.e. these folders are on the same level
	// if you want to use the file with the form as a separate file (without a folder) and it will be at the same level with the folder 'clients_documents',
	// then the line of code below will be: $uploaddir = '/'.$uploaddir.'/'.$today;
	$uploaddir = '../'.$uploaddir.'/'.$today;

	// in the following lines we check whether the files are uploaded in fact by the user
	foreach($files as $file) {if(!empty($file['name'])) {array_push($filesName, $file['name']);}}
	count($filesName)>0 ? $resultUpload = true : $resultUpload = false;

	// function for transliteration of Cyrillic characters, spaces and others in the name of the downloaded files
	function transl($st,$code='utf-8') {
		$st = mb_strtolower($st, $code);
		$st = str_replace(array(
			'?','!',',',':',';','*','(',')','{','}','%','#','№','@','$','^','-','+','/','\\','=','|','"','\'','&','а','б','в','г','д','е','ё','з','и','й','к','л','м','н','о','п','р','с','т','у','ф','х','ъ','ы','э',' ','ж','ц','ч','ш','щ','ь','ю','я'
		), array(
			'','','','','','','','','','','','','','','','','','','','','','','','','','a','b','v','g','d','e','e','z','i','y','k','l','m','n','o','p','r','s','t','u','f','h','j','i','e','_','zh','ts','ch','sh','shch','','yu','ya'  
		), $st);
		return $st;
	}

	// if at some point you need debugging - uncomment the line below
	// echo "<pre>"; print_r($files); echo "</pre>";

	if($resultUpload) { // if the files were downloaded
		if(!is_dir($uploaddir)) mkdir($uploaddir, 0755, true); // check if a directory has been created for uploading files
		foreach($files as $file) { // sorting out an array of files
			if(!empty($file['name'])) {
				// we check each file for compliance with the "good" extensions that we set in the settings above
				if(in_array(strrchr(mb_strtolower($file['name']),'.'), $goodFiles)) { 
					// move the files from the temporary folder of the server, at the same time we produce transliteration
					if(move_uploaded_file($file['tmp_name'], $uploaddir.'/'.transl($file['name']))) { 
						$files[] = realpath($uploaddir.$file['name']);
						// for security, all files are read-only
						chmod($uploaddir.'/'.transl($file['name']), 0444); 
					}
				}
				else { // if there was an attempt to download a file with a "bad" extension
					$loadError = true;
					$extension = strrchr($file['name'],'.'); 
					array_push($extensionArr, $file['name']); // form an array with "bad" extensions
					echo "<style>b{font-weight:bold!important}</style>"; // output message to the user
					echo "<b>{$file['name']}</b> - This file is not loaded, because it is forbidden to download files with the extension <b>{$extension}</b><br>";
				}
			}
		}
		if($loadError) {
			echo "<br>All other files successfully uploaded!";
			$badExtensions = implode("\n", $extensionArr);
			$htmlStart = '<!DOCTYPE html><html lang="ru"><head><meta charset="utf-8"><body><pre><h2>';
			$htmlEnd = '</h2></pre></body></html>';
			// form the file in the folder with the downloaded files, it was an attempt to download files with "bad" extensions and specify which files are specifically
			FILE_put_contents($uploaddir.'/info.txt', $htmlStart."The client tried to download the banned files:\n{$badExtensions}".$htmlEnd, FILE_APPEND);
		}
		// Below we form a line with a link for the browser so that we can view and download the client files
		if(!$ftpAccess) {
			$message .= "<b>The path to the files sent by the client:</b><br>"."ftp://{$ftpLogin}:{$ftpPass}@{$ftpHost}".'/'.$today."/<hr>"."\n";
		}
		else {
			$message .= "<b>The path to the files sent by the client:</b><br>"."ftp://{$ftpLogin}:{$ftpPass}@{$ftpHost}".strstr($uploaddir,'/')."/<hr>"."\n";
		}
	}

	$subject  = "=?utf-8?B?".base64_encode($subject)."?=";
	$headers  = "From: $from\r\nReply-to: $from\r\nContent-type: text/html; charset=utf-8\r\n";

	// below form the logs, in which we write all the information entered by the client, as well as information about the attempt to download the "bad" files
	$logText = strip_tags($message);
	$logFile = strstr($uploaddir, '/'.$today, true)."/mail.log";
	FILE_put_contents($logFile, "\n{$today}\n{$logText}\n", FILE_APPEND);
	if($loadError) FILE_put_contents($logFile, "{$today} there was an attempt to download forbidden files:\n{$badExtensions}\n", FILE_APPEND);
	chmod($logFile, 0600);
	
	if(mail($to, $subject, $message, $headers)) { // send a letter to the administrator with the data from the form filled by the client
		$subj  = "Your application is accepted in ****************"; // instead of asterisks we put the name of the organization
		$subj  = "=?utf-8?B?".base64_encode($subj)."?=";
		$mess  = "{$name}, Your application is accepted in ******************.\n<br>Expect a call to your phone number {$tel} within 8 working hours.\n\n<br><br>Yours faithfully,\n<br>COMPANY NAME\n<br>+7 (***) ***-**-**\n<br>+7 (***) ***-**-**\n<br>https://www.*******.ru";
		mail($email, $subj, $mess, $headers); // Additionally send a letter to the client that his application is accepted
		echo "<div class='general_form_sended'><b>{$name}</b>, Your application is accepted in **************.<br>Expect a call to your phone number {$tel} within 8 working hours. A letter with our contact information is sent to your e-mail {$email}</div>";
		echo "<form class='hidden'>";
	}
}
else {
	echo "<form action='#' class='general_form' method='POST' enctype='multipart/form-data'>";
}
?>
	<!-- All input fields listed below are shown as an example -->
	<div>
		<p>Briefly describe the problem and the purpose of involving an expert organization</p>
		<textarea rows="5" name="problem"></textarea>
	</div>
	<div>
		<p>Briefly describe the object (location, area, volume, number of storeys, length, project, section of the project, separate construction, construction materials, estimate documentation, initial permitting documentation, business plans, intangible assets, etc.)</p>
		<textarea rows="5" name="object"></textarea>
	</div>
	<div>
		<p>List the available documentation for the facility (design and estimate documentation, technological passport, contracts, annexes, plans, etc.)</p>
		<textarea rows="5" name="documents"></textarea>
	</div>
	<div>
		<p>If you have additional information, including photos, please specify it here</p>
		<textarea rows="5" name="dop_info"></textarea>
	</div>
	<div>
		<p>Formulate the tasks (questions) that you want to put on the permission of experts</p>
		<textarea rows="5" name="questions"></textarea>
	</div>
	<div>
		<p>Attach files (you can attach up to 10 files to a message)</p>
		<p>Allowed file types to upload: <?=implode(", ",$goodFiles)?></p>
		<br>
		<input type="file" name="file0"><input type="file" name="file1">
		<input type="file" name="file2"><input type="file" name="file3">
		<input type="file" name="file4"><input type="file" name="file5">
		<input type="file" name="file6"><input type="file" name="file7">
		<input type="file" name="file8"><input type="file" name="file9">
	</div>
	<fieldset>
		<legend>Please, specify the contact information for communication with you</legend>
		<div>
			<p>The contact person *</p>
			<input name="name" required pattern="^[а-яА-ЯёЁ\s]+$">
		</div>
		<div>
			<p>Phone *</p>
			<input name="tel" type="tel" required>
		</div>
		<div>
			<p>E-mail *</p>
			<input name="email" type="email" required>
		</div>
		<div>
			<p>Name of the organization</p>
			<input name="ooo">
		</div>
	</fieldset>
	<button type="submit">Send an application</button>
</form>

?>


CSS:

.general_form, .general_form * {-webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box;}
.general_form {margin-top: 20px; margin-bottom: 30px;}
.general_form legend, .general_form fieldset {text-align: center;}
.general_form fieldset {margin-top: 40px; background: rgba(17, 61, 124, .05);}
.general_form fieldset > div {width: 90%; margin-left: 5%; margin-bottom: 15px;}
.general_form fieldset > div:last-child {margin-bottom: 20px;}
.general_form p {margin-bottom: 3px;}
.general_form fieldset p {margin-bottom: 0;}
.general_form input {line-height: 25px; padding-top: 2px !important; padding-bottom: 2px !important;}
.general_form textarea, .general_form input:not([type="file"]) {width: 100%; border-radius: 4px; border: 1px solid rgba(17, 61, 124, 0.5); box-shadow: 0 0 1px #113d7c; padding: 10px;}
.general_form input[type="file"] {display: inline-block; width: 50%; float: left;}
.general_form > div {margin-bottom: 20px; overflow: hidden;}
.general_form button {display: block; margin: 25px auto 35px; padding: 10px 30px; cursor: pointer; background: #113D7C; color: #fff; border: 3px solid #fff; border-radius: 5px; font-size: 18px; transition: box-shadow .25s;}
.general_form button:hover {box-shadow: 0 0 2px 2px #113d7c;}
.general_form_sended {margin-top: 20px; margin-bottom: 30px; padding: 20px; border-radius: 5px; border: 2px solid green; font-size: 17px; line-height: 1.35;}
.general_form_sended b {font-weight: bold !important;}
@media(max-width: 767px) {.general_form input[type="file"] {width: 100%; margin-bottom: 10px;}}


SCRIPTS:

They are only needed for effects and sending the event to the Metric. You can not use them.

If you still want to use, then your site should have a jQuery library.

$('form.general_form textarea, form.general_form input').on('focus', function() {
	$('form.general_form p').css({'font-weight':'normal', 'color':'#000', 'letter-spacing': 'normal'});
	$(this).prev('p').css({'font-weight':'bold', 'color':'#113D7C', 'letter-spacing': '-.55px'});
	if(this.tagName=='TEXTAREA'){$('form.general_form textarea').css('background','white');$(this).css('background','aliceblue')};
})