From d5506d8cbd58045e5b815f04c216c9ad2d292b56 Mon Sep 17 00:00:00 2001 From: Banana Date: Sun, 22 Dec 2019 14:40:15 +0100 Subject: [PATCH] rewrite the creation and lokkup process --- TODO | 4 +- documentation/security.txt | 5 +- webroot/index.php | 42 +++--- webroot/lib/mancubus.class.php | 237 +++++++++++++++++++++++++++++++++ webroot/lib/summoner.class.php | 198 ++++++++------------------- 5 files changed, 319 insertions(+), 167 deletions(-) create mode 100644 webroot/lib/mancubus.class.php diff --git a/TODO b/TODO index 8483a7e..97f40df 100644 --- a/TODO +++ b/TODO @@ -4,4 +4,6 @@ * add force download parameter * HTTPS for selfpaste bash client * multiple secrets -* extending allowed filetypes \ No newline at end of file +* extending allowed filetypes +* creation or even access with basic auth +* flood protection \ No newline at end of file diff --git a/documentation/security.txt b/documentation/security.txt index 1cf218b..dea5fd1 100644 --- a/documentation/security.txt +++ b/documentation/security.txt @@ -25,4 +25,7 @@ So, here is a friendly REMINDER: - You provide the service by hosting it. Your are responsible for it! - Change your secret often -Make sure DEBUG is false for production. \ No newline at end of file +Make sure DEBUG is false for production. + +Protect the storage location from direct access. Default solved with a .htaccess +file. Better solution is to move the location outside the webroot. \ No newline at end of file diff --git a/webroot/index.php b/webroot/index.php index a390cd4..51ff9d7 100644 --- a/webroot/index.php +++ b/webroot/index.php @@ -40,9 +40,11 @@ else { } # static helper class -require 'lib/summoner.class.php'; +require_once 'lib/summoner.class.php'; # config file -require 'config.php'; +require_once 'config.php'; +# upload / file handling +require_once 'lib/mancubus.class.php'; $_short = false; if(isset($_GET['s']) && !empty($_GET['s'])) { @@ -67,8 +69,12 @@ if(!empty($_short)) { $httpResponseCode = 404; $contentBody = 'File not found.'; - $_requestFile = Summoner::createStoragePath($_short); - $_requestFile .= $_short; + $_t = Summoner::b64sl_unpack_id($_short); + $_t = (string)$_t; + $_p = Summoner::forwardslashStringToPath($_t); + $_requestFile = Summoner::endsWith(SELFPASTE_UPLOAD_DIR,'/') ? SELFPASTE_UPLOAD_DIR : SELFPASTE_UPLOAD_DIR.'/'; + $_requestFile .= $_p; + $_requestFile .= $_t; if(is_readable($_requestFile)) { $contentBody = $_requestFile; $httpResponseCode = 200; @@ -82,23 +88,17 @@ elseif ($_create === true) { $_file = $_FILES['pasty']; - $_file['short'] = Summoner::createShort(); - $_file['shortUrl'] = SELFPASTE_URL.'/'.$_file['short']; - $_file['storagepath'] = Summoner::createStoragePath($_file['short']); - - $_checks = array('checkFileUploadStatus','checkAllowedFiletype','checkStorage','moveUploadedPasteFile'); - $_do['status'] = false; - foreach($_checks as $_check) { - if(method_exists('Summoner',$_check)) { - $_do = Summoner::$_check($_file); - $_message = $_do['message']; - if($_do['status'] === true) { - $httpResponseCode = 200; - } - else { - $httpResponseCode = 400; - break; - } + $_fileObj = new Mancubus(); + if($_fileObj->load($_FILES['pasty']) === true) { + $_fileObj->setSaveFilename(); + $_fileObj->setShort(); + $_fileObj->setStoragePath(); + $_fileObj->setShortURL(); + + $_do = $_fileObj->process(); + $_message = $_do['message']; + if($_do['status'] === true) { + $httpResponseCode = 200; } } diff --git a/webroot/lib/mancubus.class.php b/webroot/lib/mancubus.class.php new file mode 100644 index 0000000..a10e661 --- /dev/null +++ b/webroot/lib/mancubus.class.php @@ -0,0 +1,237 @@ +_uploadedData = $file; + $ret = true; + } + + return $ret; + } + + /** + * Either set short to given string + * or create from _saveFilename. In this case _saveFilename is a number + * @param string $short + */ + public function setShort($short='') { + if($short != '') { + $this->_short = $short; + } + elseif(!empty($this->_saveFilename)) { + $this->_short = Summoner::b64sl_pack_id($this->_saveFilename); + } + } + + /** + * Either set _saveFilename to given string + * or create from a random number. In this case _short needs this as a base + * @param string $string + * @throws Exception + */ + public function setSaveFilename($string='') { + if($string != '') { + $this->_saveFilename = $string; + } + else { + $r = random_int(1000, 9999); + $this->_saveFilename = (string)$r; + } + } + + /** + * Set _shortURL to given string + * or create based on SELFPASTE_URL and _short + * @param string $string + */ + public function setShortURL($string='') { + if($string != '') { + $this->_shortURL = $string; + } + elseif(!empty($this->_short)) { + $this->_shortURL = SELFPASTE_URL.'/'.$this->_short; + } + } + + /** + * set the right storage path based on _saveFilename + * and SELFPASTE_UPLOAD_DIR + */ + public function setStoragePath() { + $string = $this->_saveFilename; + + if(!empty($string)) { + $p = SELFPASTE_UPLOAD_DIR.'/'; + $p .= Summoner::forwardslashStringToPath($string); + $this->_storagePath = $p; + } + } + + public function process() { + $ret = array( + 'message' => '', + 'status' => false + ); + + try { + $ret = $this->_checkFileUploadStatus(); + $ret = $this->_checkAllowedFiletype(); + $ret = $this->_checkStorage(); + $ret = $this->_moveUploadedFile(); + } + catch (Exception $e) { + $ret['message'] = $e->getMessage(); + } + + return $ret; + } + + /** + * Check if the POST upload worked + * @return array message,status + * @throws Exception + */ + private function _checkFileUploadStatus() { + $check = Summoner::checkFileUploadStatus($this->_uploadedData['error']); + + if($check['status'] === true) { + # check has the structure we want already + return $check; + } + else { + throw new Exception($check['message']); + } + } + + /** + * Check if the uploaded file matches the allowed filetypes + * @return array message,status + * @throws Exception + */ + private function _checkAllowedFiletype() { + $message = "Filetype not supported"; + $status = false; + + $workWith = $this->_uploadedData['tmp_name']; + if(!empty($workWith)) { + $finfo = finfo_open(FILEINFO_MIME_TYPE); + $mime = finfo_file($finfo, $workWith); + finfo_close($finfo); + if(strpos(SELFPASTE_ALLOWED_FILETYPES,$mime) !== false) { + $status = true; + $message = "Filetype allowed"; + } + else { + if(DEBUG) $message .= " $mime"; + throw new Exception($message); + } + } else { + throw new Exception($message); + } + + return array( + 'message' => $message, + 'status' => $status + ); + } + + private function _checkStorage() { + $message = "File storage failure"; + $status = false; + + $workwith = $this->_storagePath; + if(is_writable(SELFPASTE_UPLOAD_DIR)) { + if (mkdir($workwith,0777,true)) { + $message = "File storage creation success"; + $status = true; + } + else { + if(DEBUG) $message .= " ".$workwith; + throw new Exception($message); + } + } + else { + throw new Exception('Storage location not writeable'); + } + + return array( + 'message' => $message, + 'status' => $status + ); + } + + /** + * Move the tmp_file from _uploadedData to the new location + * provided by _storagePath and _saveFilename + * @return array + * @throws Exception + */ + private function _moveUploadedFile() { + $message = "File storage failure"; + $status = false; + + $workwithPath = $this->_storagePath; + $workwithFilename = $this->_saveFilename; + + if(!empty($workwithPath) && !empty($workwithFilename)) { + $_newFilename = Summoner::endsWith($workwithPath,'/') ? $workwithPath : $workwithPath.'/'; + $_newFilename .= $workwithFilename; + if(move_uploaded_file($this->_uploadedData['tmp_name'], $_newFilename)) { + $status = true; + $message = $this->_shortURL; + } + else { + if(DEBUG) $message .= " $_newFilename"; + throw new Exception($message); + } + } + else { + throw new Exception('Failing requirements for saving'); + } + + return array( + 'message' => $message, + 'status' => $status + ); + } +} \ No newline at end of file diff --git a/webroot/lib/summoner.class.php b/webroot/lib/summoner.class.php index f05aaf8..7ac3c46 100644 --- a/webroot/lib/summoner.class.php +++ b/webroot/lib/summoner.class.php @@ -139,132 +139,41 @@ class Summoner { /** - * Simple helper to detect the $_FILE upload status - * Expects an array from $_FILE - * @param $file + * Simple helper to detect the $_FILES upload status + * Expects the error value from $_FILES['error'] + * @param $error * @return array */ - static function checkFileUploadStatus($file) { + static function checkFileUploadStatus($error) { $message = "Unknown upload error"; $status = false; - if(isset($file['error'])) { - switch ($file['error']) { - case UPLOAD_ERR_OK: - $message = "There is no error, the file uploaded with success."; - $status = true; - break; - case UPLOAD_ERR_INI_SIZE: - $message = "The uploaded file exceeds the upload_max_filesize directive in php.ini"; - break; - case UPLOAD_ERR_FORM_SIZE: - $message = "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form"; - break; - case UPLOAD_ERR_PARTIAL: - $message = "The uploaded file was only partially uploaded"; - break; - case UPLOAD_ERR_NO_FILE: - $message = "No file was uploaded"; - break; - case UPLOAD_ERR_NO_TMP_DIR: - $message = "Missing a temporary folder"; - break; - case UPLOAD_ERR_CANT_WRITE: - $message = "Failed to write file to disk"; - break; - case UPLOAD_ERR_EXTENSION: - $message = "File upload stopped by extension"; - break; - } - } - - return array( - 'message' => $message, - 'status' => $status - ); - } - - /** - * Simple helper to detect the $_FILE type - * Expects an array from $_FILE - * - * @see https://www.php.net/manual/en/intro.fileinfo.php - * - * @param $file - * @return array - */ - static function checkAllowedFiletype($file) { - $message = "Filetype not supported"; - $status = false; - - if(isset($file['tmp_name'])) { - $finfo = finfo_open(FILEINFO_MIME_TYPE); - $mime = finfo_file($finfo, $file['tmp_name']); - finfo_close($finfo); - if(strpos(SELFPASTE_ALLOWED_FILETYPES,$mime) !== false) { - $status = true; - $message = "Filetype allowed"; - } - if(DEBUG) $message .= " $mime"; - } - - return array( - 'message' => $message, - 'status' => $status - ); - } - - /** - * Simple helper to create and make sure the storage - * location is available - * Expects an array from $_FILE - * with an extra key = storagepath - * - * @param $file - * @return array - */ - static function checkStorage($file) { - $message = "File storage failure"; - $status = false; - - if(isset($file['storagepath']) && !empty($file['storagepath']) - && is_writable(SELFPASTE_UPLOAD_DIR)) { - if (mkdir($file['storagepath'],0777,true)) { - $message = "File storage creation success"; + switch ($error) { + case UPLOAD_ERR_OK: + $message = "There is no error, the file uploaded with success."; $status = true; - } - } - - if(DEBUG) $message .= " ".$file['storagepath']; - - return array( - 'message' => $message, - 'status' => $status - ); - } - - /** - * move the uploaded file. - * Depends on the _FILES info and the keys - * storagepath, short, shortUrl - * @param $file - * @return array - */ - static function moveUploadedPasteFile($file) { - $message = "File storage failure"; - $status = false; - //shortUrl - - if(isset($file['storagepath']) && !empty($file['storagepath']) - && isset($file['short']) && !empty($file['short'])) { - $_newFilename = self::endsWith($file['storagepath'],'/') ? $file['storagepath'] : $file['storagepath'].'/'; - $_newFilename .= $file['short']; - if(move_uploaded_file($file['tmp_name'], $_newFilename)) { - $status = true; - $message = $file['shortUrl']; - } - - if(DEBUG) $message .= " $_newFilename"; + break; + case UPLOAD_ERR_INI_SIZE: + $message = "The uploaded file exceeds the upload_max_filesize directive in php.ini"; + break; + case UPLOAD_ERR_FORM_SIZE: + $message = "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form"; + break; + case UPLOAD_ERR_PARTIAL: + $message = "The uploaded file was only partially uploaded"; + break; + case UPLOAD_ERR_NO_FILE: + $message = "No file was uploaded"; + break; + case UPLOAD_ERR_NO_TMP_DIR: + $message = "Missing a temporary folder"; + break; + case UPLOAD_ERR_CANT_WRITE: + $message = "Failed to write file to disk"; + break; + case UPLOAD_ERR_EXTENSION: + $message = "File upload stopped by extension"; + break; } return array( @@ -273,22 +182,10 @@ class Summoner { ); } - /** - * Simple helper to create a new name - * - * @return string - * @throws Exception - */ - static function createShort() { - $idstring = random_int(1000, 9999); - return self::b64sl_pack_id($idstring); - } - /** * create a short string based on a integer * * @see https://www.jwz.org/base64-shortlinks/ - * * @return string */ static function b64sl_pack_id($id) { @@ -304,24 +201,37 @@ class Summoner { return $id; } + /** + * Decode a base64-encoded big-endian integer of up to 64 bits. + * + * @see https://www.jwz.org/base64-shortlinks/ + * @param $id + * @return false|int|string|string[] + */ + static function b64sl_unpack_id($id) { + $id = str_replace ('-', '+', $id); // decode URL-unsafe "+" "/" + $id = str_replace ('_', '/', $id); + $id = base64_decode ($id); + while (strlen($id) < 8) { $id = "\000$id"; } // pad with leading NULs + $a = unpack ('N*', $id); // 32 bit big endian + $id = ($a[1] << 32) | $a[2]; // pack top and bottom word + return $id; + } + /** * create based on the given string a path * each char in string is a dir - * and add SELFPASTE_UPLOAD_DIR - * asd -> SELFPASTE_UPLOAD_DIR/a/s/d + * asdef -> a/s/d/e/f/ * @param $string - * @return bool|string + * @return string */ - static function createStoragePath($string) { - $p = false; - - if(!empty($string) && is_writable(SELFPASTE_UPLOAD_DIR)) { - $p = SELFPASTE_UPLOAD_DIR.'/'; - for($i=0;$i