Created
May 31, 2025 22:43
-
-
Save perryflynn/6fc08c278df6fe0aa83ef275392edca5 to your computer and use it in GitHub Desktop.
Handling securely a PHP file upload
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
declare(strict_types=1); | |
namespace PerrysFramework; | |
class FileUploadUtils | |
{ | |
// This is just a list of my personal common mime types, | |
// this may not suit your requirements | |
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/MIME_types/Common_types | |
const FileTypeMap = [ | |
// images | |
'image/png' => '.png', | |
'image/jpeg' => '.jpg', | |
'image/gif' => '.gif', | |
'image/webp' => '.webp', | |
'image/svg+xml' => '.svg', | |
'image/svg' => '.svg', | |
// documents | |
'application/pdf' => '.pdf', | |
// text | |
'text/plain' => '.txt', | |
'text/css' => '.css', | |
'text/csv' => '.csv', | |
'text/html' => '.html', | |
'text/javascript' => '.js', | |
'application/json' => '.json', | |
'text/markdown' => '.md', | |
// archives | |
'application/gzip' => '.gz', | |
'application/x-gzip' => '.gz', | |
'application/x-tar' => '.tar', | |
'application/zip' => '.zip', | |
'application/x-zip-compressed' => '.zip', | |
]; | |
/** | |
* Handle file upload | |
* @param fileItem Element from $_FILE | |
* @param target Full path to store uploaded file without extension | |
* @param maxFileSize Maximum file size in bytes | |
* @param mimeTypePrefix Detected file mime type must start with or equal to this string | |
* @param mimeMap Map mime type to file extension | |
* @param ensureInMimeMap Files mime type MUST exist in mimeMap | |
* @param allowOverride Allows overriding existing files | |
*/ | |
public static function receiveUploadedFile(array $fileItem, string $target, | |
int $maxFileSize = 0, ?string $mimeTypePrefix = null, | |
array $mimeMap = self::FileTypeMap, bool $ensureInMimeMap = true, | |
bool $allowOverride = false) | |
{ | |
// https://dev.to/einlinuus/how-to-upload-files-with-php-correctly-and-securely-1kng | |
// https://www.php.net/manual/en/features.file-upload.post-method.php | |
// handle error codes from $_FILE item | |
if (!array_key_exists('error', $fileItem)) | |
{ | |
return [ false, 'php_no_error_info', 'There was no error info in the file array', null, null, null, null, null ]; | |
} | |
else if ($fileItem['error'] == UPLOAD_ERR_CANT_WRITE) | |
{ | |
return [ false, 'php_cant_write', 'Failed to write upload to disk', null, null, null, null, null ]; | |
} | |
else if ($fileItem['error'] == UPLOAD_ERR_EXTENSION) | |
{ | |
return [ false, 'php_stopped_by_extension', 'Upload was stopped by PHP extension', null, null, null, null, null ]; | |
} | |
else if ($fileItem['error'] == UPLOAD_ERR_FORM_SIZE) | |
{ | |
return [ false, 'php_too_large_form', 'File larger than defined in form', null, null, null, null, null ]; | |
} | |
else if ($fileItem['error'] == UPLOAD_ERR_INI_SIZE) | |
{ | |
return [ false, 'php_too_large_ini', 'File larger than defined in php.ini', null, null, null, null, null ]; | |
} | |
else if ($fileItem['error'] == UPLOAD_ERR_NO_FILE) | |
{ | |
return [ false, 'php_no_file', 'No file was uploaded', null, null, null, null, null ]; | |
} | |
else if ($fileItem['error'] == UPLOAD_ERR_NO_TMP_DIR) | |
{ | |
return [ false, 'php_no_tmp_dir', 'Temporary folder was missing', null, null, null, null, null ]; | |
} | |
else if ($fileItem['error'] == UPLOAD_ERR_PARTIAL) | |
{ | |
return [ false, 'php_partial', 'File was only uploaded partially', null, null, null, null, null ]; | |
} | |
if ($fileItem['error'] !== UPLOAD_ERR_OK) | |
{ | |
return [ false, 'php_unexpected_upload_error', 'Unhandled PHP upload error', null, null, null, null, null ]; | |
} | |
// file must exist | |
$tempFile = null; | |
if (isset($fileItem['tmp_name']) && is_file($fileItem['tmp_name'])) | |
{ | |
$tempFile = $fileItem['tmp_name']; | |
} | |
if (empty($tempFile)) | |
{ | |
return [ false, 'no_file', 'File could not be found in temporary folder', null, null, null, null, null ]; | |
} | |
// file must be an uploaded file | |
if (!is_uploaded_file($tempFile)) | |
{ | |
return [ false, 'not_uploaded', 'File is not an uploaded file', null, null, null, null, null ]; | |
} | |
// check file size | |
$fileSize = filesize($tempFile); | |
if ($fileSize <= 0) | |
{ | |
return [ false, 'file_empty', 'File is empty', null, null, null, null, null ]; | |
} | |
if ($maxFileSize > 0 && $fileSize > $maxFileSize) | |
{ | |
return [ false, 'file_too_large', 'File larger than '.$maxFileSize.' bytes', null, null, null, null, null ]; | |
} | |
// get mime type from file | |
$fileInfo = finfo_open(FILEINFO_MIME_TYPE); | |
$fileType = finfo_file($fileInfo, $tempFile); | |
if (!empty($mimeTypePrefix) && empty($fileType)) | |
{ | |
return [ false, 'unknown_filetype', 'Unable to determine file type', null, null, null, null, null ]; | |
} | |
if (!empty($mimeTypePrefix) && strpos($fileType, $mimeTypePrefix) !== 0) | |
{ | |
return [ false, 'unexpected_filetype', 'Unexpected file type '.$fileType, null, null, null, null, null ]; | |
} | |
// find file extension by mime | |
$fileExtension = null; | |
if (count($mimeMap) > 0) | |
{ | |
$mimeResult = array_filter($mimeMap, function($mapMime) use($fileType) | |
{ | |
return $mapMime == $fileType || strpos($fileType, $mapMime.';') === 0; | |
}, | |
ARRAY_FILTER_USE_KEY); | |
if (count($mimeResult) > 0) | |
{ | |
$fileExtension = reset($mimeResult); | |
} | |
} | |
if ($ensureInMimeMap && empty($fileExtension)) | |
{ | |
return [ false, 'not_in_mimemap', 'Type '.$fileType.' was not found in mime map', null, null, null, null, null ]; | |
} | |
// use detected extension in filename | |
$name = 'untitled.ukn'; | |
if (isset($fileItem['name']) && !empty($fileItem['name'])) | |
{ | |
$name = $fileItem['name']; | |
} | |
$nameNoExt = pathinfo($name, PATHINFO_FILENAME); | |
$finalName = $name; | |
$finalTarget = $target; | |
if (!empty($nameNoExt) && !empty($fileExtension)) | |
{ | |
$finalName = $nameNoExt.$fileExtension; | |
$finalTarget = $finalTarget.$fileExtension; | |
} | |
// prevent overriding | |
if (!$allowOverride && file_exists($finalTarget)) | |
{ | |
return [ false, 'file_exists', 'File '.$finalTarget.' already exists', null, null, null, null, null ]; | |
} | |
// move uploaded file to requested destination | |
if (!move_uploaded_file($tempFile, $finalTarget)) | |
{ | |
return [ false, 'move_failed', 'Moving uploaded file to '.$finalTarget.' failed', null, null, null, null, null ]; | |
} | |
return [ true, 'ok', 'Success', $finalName, $finalTarget, $fileExtension, $fileType, $fileSize ]; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment