diff --git a/ProcessMaker/Http/Controllers/Api/FileController.php b/ProcessMaker/Http/Controllers/Api/FileController.php index 8cd1de0171..2aa2e217b6 100644 --- a/ProcessMaker/Http/Controllers/Api/FileController.php +++ b/ProcessMaker/Http/Controllers/Api/FileController.php @@ -13,10 +13,13 @@ use ProcessMaker\Models\MediaLog; use ProcessMaker\Models\ProcessRequest; use ProcessMaker\Models\TaskDraft; +use ProcessMaker\Traits\ValidatesFileTrait; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class FileController extends Controller { + use ValidatesFileTrait; + /** * A whitelist of attributes that should not be * sanitized by our SanitizeInput middleware. @@ -188,7 +191,21 @@ public function store(Request $request) } $mediaCollection = $request->input('collection', 'local'); + + // Validate the file before processing + $uploadedFile = $request->file('file'); + if (!$uploadedFile) { + return abort(response(['message' => 'No file provided'], 422)); + } + + $errors = []; + $this->validateFile($uploadedFile, $errors); + if (count($errors) > 0) { + return abort(response($errors, 422)); + } + $file = $model->addMediaFromRequest('file'); + $user = pmUser(); $originalCreatedBy = $user ? $user->id : null; $data_name = $request->input('data_name', ''); diff --git a/ProcessMaker/Http/Controllers/Api/ProcessRequestFileController.php b/ProcessMaker/Http/Controllers/Api/ProcessRequestFileController.php index 1e606ca5f4..730da17d32 100644 --- a/ProcessMaker/Http/Controllers/Api/ProcessRequestFileController.php +++ b/ProcessMaker/Http/Controllers/Api/ProcessRequestFileController.php @@ -3,17 +3,13 @@ namespace ProcessMaker\Http\Controllers\Api; use Exception; -use Illuminate\Contracts\Routing\ResponseFactory; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\ResourceCollection; use Illuminate\Http\Response; use Illuminate\Http\UploadedFile; -use Illuminate\Support\Facades\Auth; -use Illuminate\Support\Facades\Storage; use Pion\Laravel\ChunkUpload\Exceptions\UploadMissingFileException; use Pion\Laravel\ChunkUpload\Handler\AbstractHandler; -use Pion\Laravel\ChunkUpload\Handler\HandlerFactory; use Pion\Laravel\ChunkUpload\Receiver\FileReceiver; use ProcessMaker\Events\FilesAccessed; use ProcessMaker\Events\FilesCreated; @@ -21,14 +17,16 @@ use ProcessMaker\Events\FilesDownloaded; use ProcessMaker\Http\Controllers\Controller; use ProcessMaker\Http\Resources\ApiCollection; -use ProcessMaker\Http\Resources\ApiResource; use ProcessMaker\Models\Media; use ProcessMaker\Models\ProcessRequest; use ProcessMaker\Models\TaskDraft; +use ProcessMaker\Traits\ValidatesFileTrait; use Spatie\MediaLibrary\MediaCollections\Exceptions\FileIsTooBig; class ProcessRequestFileController extends Controller { + use ValidatesFileTrait; + /** * A whitelist of attributes that should not be * sanitized by our SanitizeInput middleware. @@ -439,126 +437,4 @@ public function destroy(Request $laravel_request, ProcessRequest $request, $file return response([], 204); } - - /** - * Validate uploaded file for security and type restrictions - * - * @param UploadedFile $file - * @param array $errors - * @return array - */ - private function validateFile(UploadedFile $file, &$errors) - { - // Explicitly reject archive files for security - if (config('files.enable_dangerous_validation')) { - $this->rejectArchiveFiles($file, $errors); - } - - // Validate file extension if enabled - if (config('files.enable_extension_validation')) { - $this->validateFileExtension($file, $errors); - } - - // Validate MIME type vs extension if enabled - if (config('files.enable_mime_validation')) { - $this->validateExtensionMimeTypeMatch($file, $errors); - } - - // Validate specific file types (e.g., PDF for JavaScript content) - if (strtolower($file->getClientOriginalExtension()) === 'pdf') { - $this->validatePDFFile($file, $errors); - } - - return $errors; - } - - /** - * Explicitly reject archive files for security reasons - * - * @param UploadedFile $file - * @param array $errors - * @return void - */ - private function rejectArchiveFiles(UploadedFile $file, &$errors) - { - $dangerousExtensions = config('files.dangerous_extensions'); - - $fileExtension = strtolower($file->getClientOriginalExtension()); - - if (in_array($fileExtension, $dangerousExtensions)) { - $errors['message'] = __('Uploaded file type is not allowed'); - - return; - } - - // Also check MIME types for archive files - $dangerousMimeTypes = config('files.dangerous_mime_types'); - - $fileMimeType = $file->getMimeType(); - - if (in_array($fileMimeType, $dangerousMimeTypes)) { - $errors['message'] = __('Uploaded mime file type is not allowed'); - } - } - - /** - * Validate that file extension matches the MIME type - * - * @param UploadedFile $file - * @param array $errors - * @return void - */ - private function validateExtensionMimeTypeMatch(UploadedFile $file, &$errors) - { - $fileExtension = strtolower($file->getClientOriginalExtension()); - $fileMimeType = $file->getMimeType(); - - // Get extension to MIME type mapping from configuration - $extensionMimeMap = config('files.extension_mime_map'); - - // Check if extension exists in our map - if (!isset($extensionMimeMap[$fileExtension])) { - $errors['message'] = __('File extension not allowed'); - - return; - } - - // Check if MIME type matches any of the expected types for this extension - if (!in_array($fileMimeType, $extensionMimeMap[$fileExtension])) { - $errors['message'] = __('The file extension does not match the actual file content'); - } - } - - /** - * Validate file extension against allowed extensions - * - * @param UploadedFile $file - * @param array $errors - * @return void - */ - private function validateFileExtension(UploadedFile $file, &$errors) - { - $allowedExtensions = config('files.allowed_extensions'); - $fileExtension = strtolower($file->getClientOriginalExtension()); - - if (!in_array($fileExtension, $allowedExtensions)) { - $errors['message'] = __('File extension not allowed'); - } - } - - private function validatePDFFile(UploadedFile $file, &$errors) - { - $text = $file->get(); - - $jsKeywords = ['/JavaScript', '<< /S /JavaScript']; - - foreach ($jsKeywords as $keyword) { - if (strpos($text, $keyword) !== false) { - $errors[] = __('Dangerous PDF file content'); - break; - } - } - - return $errors; - } } diff --git a/ProcessMaker/Traits/ValidatesFileTrait.php b/ProcessMaker/Traits/ValidatesFileTrait.php new file mode 100644 index 0000000000..358df85e8a --- /dev/null +++ b/ProcessMaker/Traits/ValidatesFileTrait.php @@ -0,0 +1,136 @@ +rejectArchiveFiles($file, $errors); + } + + // Validate file extension if enabled + if (config('files.enable_extension_validation', true)) { + $this->validateFileExtension($file, $errors); + } + + // Validate MIME type vs extension if enabled + if (config('files.enable_mime_validation', true)) { + $this->validateExtensionMimeTypeMatch($file, $errors); + } + + // Validate specific file types (e.g., PDF for JavaScript content) + if (strtolower($file->getClientOriginalExtension()) === 'pdf') { + $this->validatePDFFile($file, $errors); + } + + return $errors; + } + + /** + * Explicitly reject archive files for security reasons + * + * @param UploadedFile $file + * @param array $errors + * @return void + */ + private function rejectArchiveFiles(UploadedFile $file, &$errors) + { + $dangerousExtensions = config('files.dangerous_extensions'); + + $fileExtension = strtolower($file->getClientOriginalExtension()); + + if (in_array($fileExtension, $dangerousExtensions)) { + $errors['message'] = __('Uploaded file type is not allowed'); + + return; + } + + // Also check MIME types for archive files + $dangerousMimeTypes = config('files.dangerous_mime_types'); + + $fileMimeType = $file->getMimeType(); + + if (in_array($fileMimeType, $dangerousMimeTypes)) { + $errors['message'] = __('Uploaded mime file type is not allowed'); + } + } + + /** + * Validate that file extension matches the MIME type + * + * @param UploadedFile $file + * @param array $errors + * @return void + */ + private function validateExtensionMimeTypeMatch(UploadedFile $file, &$errors) + { + $fileExtension = strtolower($file->getClientOriginalExtension()); + $fileMimeType = $file->getMimeType(); + + // Get extension to MIME type mapping from configuration + $extensionMimeMap = config('files.extension_mime_map'); + + // Check if extension exists in our map + if (!isset($extensionMimeMap[$fileExtension])) { + $errors['message'] = __('File extension not allowed'); + + return; + } + + // Check if MIME type matches any of the expected types for this extension + if (!in_array($fileMimeType, $extensionMimeMap[$fileExtension])) { + $errors['message'] = __('The file extension does not match the actual file content'); + } + } + + /** + * Validate file extension against allowed extensions + * + * @param UploadedFile $file + * @param array $errors + * @return void + */ + private function validateFileExtension(UploadedFile $file, &$errors) + { + $allowedExtensions = config('files.allowed_extensions'); + + $fileExtension = strtolower($file->getClientOriginalExtension()); + + if (!in_array($fileExtension, $allowedExtensions)) { + $errors['message'] = __('File extension not allowed'); + } + } + + /** + * Validate PDF files for dangerous content + * + * @param UploadedFile $file + * @param array $errors + * @return void + */ + private function validatePDFFile(UploadedFile $file, &$errors) + { + $text = $file->get(); + + $jsKeywords = ['/JavaScript', '<< /S /JavaScript']; + + foreach ($jsKeywords as $keyword) { + if (strpos($text, $keyword) !== false) { + $errors[] = __('Dangerous PDF file content'); + break; + } + } + } +}