From cdc913d16cf624aee852bc9163a7c6ffc8d1da9d Mon Sep 17 00:00:00 2001 From: theWorstComrade <59704962+theWorstComrade@users.noreply.github.com> Date: Wed, 29 Dec 2021 13:33:20 +0100 Subject: [PATCH] Unrestricted php file upload fix (#681) https://huntr.dev/bounties/d7453360-baca-4e56-985f-481275fa38db/ --- .../V1/Admin/Expense/ExpensesController.php | 4 +- .../Admin/Expense/UploadReceiptController.php | 5 +- .../V1/Admin/Settings/CompanyController.php | 10 ++- app/Http/Requests/AvatarRequest.php | 40 +++++++++ app/Http/Requests/CompanyLogoRequest.php | 34 ++++++++ app/Http/Requests/ExpenseRequest.php | 6 ++ app/Rules/Base64Mime.php | 85 +++++++++++++++++++ 7 files changed, 176 insertions(+), 8 deletions(-) create mode 100644 app/Http/Requests/AvatarRequest.php create mode 100644 app/Http/Requests/CompanyLogoRequest.php create mode 100644 app/Rules/Base64Mime.php diff --git a/app/Http/Controllers/V1/Admin/Expense/ExpensesController.php b/app/Http/Controllers/V1/Admin/Expense/ExpensesController.php index bcb94003e..9027a1f45 100644 --- a/app/Http/Controllers/V1/Admin/Expense/ExpensesController.php +++ b/app/Http/Controllers/V1/Admin/Expense/ExpensesController.php @@ -39,7 +39,7 @@ public function index(Request $request) /** * Store a newly created resource in storage. * - * @param \Illuminate\Http\Request $request + * @param \Crater\Http\Requests\ExpenseRequest $request * @return \Illuminate\Http\JsonResponse */ public function store(ExpenseRequest $request) @@ -67,7 +67,7 @@ public function show(Expense $expense) /** * Update the specified resource in storage. * - * @param \Illuminate\Http\Request $request + * @param \Crater\Http\Requests\ExpenseRequest $request * @param \Crater\Models\Expense $expense * @return \Illuminate\Http\JsonResponse */ diff --git a/app/Http/Controllers/V1/Admin/Expense/UploadReceiptController.php b/app/Http/Controllers/V1/Admin/Expense/UploadReceiptController.php index cc7d6135c..9c58d328e 100644 --- a/app/Http/Controllers/V1/Admin/Expense/UploadReceiptController.php +++ b/app/Http/Controllers/V1/Admin/Expense/UploadReceiptController.php @@ -5,17 +5,18 @@ use Crater\Http\Controllers\Controller; use Crater\Models\Expense; use Illuminate\Http\Request; +use Crater\Http\Requests\ExpenseRequest; class UploadReceiptController extends Controller { /** * Upload the expense receipts to storage. * - * @param \Illuminate\Http\Request $request + * @param \Crater\Http\Requests\ExpenseRequest $request * @param Expense $expense * @return \Illuminate\Http\JsonResponse */ - public function __invoke(Request $request, Expense $expense) + public function __invoke(ExpenseRequest $request, Expense $expense) { $this->authorize('update', $expense); diff --git a/app/Http/Controllers/V1/Admin/Settings/CompanyController.php b/app/Http/Controllers/V1/Admin/Settings/CompanyController.php index f594e13bf..e299c76af 100644 --- a/app/Http/Controllers/V1/Admin/Settings/CompanyController.php +++ b/app/Http/Controllers/V1/Admin/Settings/CompanyController.php @@ -9,6 +9,8 @@ use Crater\Http\Resources\UserResource; use Crater\Models\Company; use Illuminate\Http\Request; +use Crater\Http\Requests\AvatarRequest; +use Crater\Http\Requests\CompanyLogoRequest; class CompanyController extends Controller { @@ -58,10 +60,10 @@ public function updateCompany(CompanyRequest $request) /** * Upload the company logo to storage. * - * @param \Illuminate\Http\Request $request + * @param \Crater\Http\Requests\CompanyLogoRequest $request * @return \Illuminate\Http\JsonResponse */ - public function uploadCompanyLogo(Request $request) + public function uploadCompanyLogo(CompanyLogoRequest $request) { $company = Company::find($request->header('company')); @@ -89,10 +91,10 @@ public function uploadCompanyLogo(Request $request) /** * Upload the Admin Avatar to public storage. * - * @param \Illuminate\Http\Request $request + * @param \Crater\Http\Requests\AvatarRequest $request * @return \Illuminate\Http\JsonResponse */ - public function uploadAvatar(Request $request) + public function uploadAvatar(AvatarRequest $request) { $user = auth()->user(); diff --git a/app/Http/Requests/AvatarRequest.php b/app/Http/Requests/AvatarRequest.php new file mode 100644 index 000000000..832a0ed77 --- /dev/null +++ b/app/Http/Requests/AvatarRequest.php @@ -0,0 +1,40 @@ + [ + 'nullable', + 'file', + 'mimes:gif,jpg,png', + 'max:20000' + ], + 'avatar' => [ + 'nullable', + new Base64Mime(['gif', 'jpg', 'png']) + ] + ]; + } +} diff --git a/app/Http/Requests/CompanyLogoRequest.php b/app/Http/Requests/CompanyLogoRequest.php new file mode 100644 index 000000000..3ff0c685e --- /dev/null +++ b/app/Http/Requests/CompanyLogoRequest.php @@ -0,0 +1,34 @@ + [ + 'nullable', + new Base64Mime(['gif', 'jpg', 'png']) + ] + ]; + } +} diff --git a/app/Http/Requests/ExpenseRequest.php b/app/Http/Requests/ExpenseRequest.php index b74f9be1e..a8baa723f 100644 --- a/app/Http/Requests/ExpenseRequest.php +++ b/app/Http/Requests/ExpenseRequest.php @@ -51,6 +51,12 @@ public function rules() 'currency_id' => [ 'required' ], + 'attachment_receipt' => [ + 'nullable', + 'file', + 'mimes:jpg,png,pdf,doc,docx,xls,xlsx,ppt,pptx', + 'max:20000' + ] ]; if ($companyCurrency && $this->currency_id) { diff --git a/app/Rules/Base64Mime.php b/app/Rules/Base64Mime.php new file mode 100644 index 000000000..bb020e786 --- /dev/null +++ b/app/Rules/Base64Mime.php @@ -0,0 +1,85 @@ +extensions = $extensions; + } + + /** + * Determine if the validation rule passes. + * + * @param string $attribute + * @param mixed $value + * @return bool + */ + public function passes($attribute, $value) + { + $this->attribute = $attribute; + + try { + $data = json_decode($value)->data; + } catch (\Exception $e) { + return False; + } + + $pattern = '/^data:\w+\/[\w\+]+;base64,[\w\+\=\/]+$/'; + + if(!preg_match($pattern, $data)) { + return False; + } + + $data = explode(',', $data); + + if(!isset($data[1]) || empty($data[1])) { + return False; + } + + try { + $data = base64_decode($data[1]); + $f = finfo_open(); + $result = finfo_buffer($f, $data, FILEINFO_EXTENSION); + + if($result === '???') + return False; + + if(strpos($result, '/')) { + foreach(explode('/', $result) as $ext) { + if(in_array($ext, $this->extensions)) + return True; + } + } else { + if(in_array($result, $this->extensions)) + return True; + } + } catch (\Exception $e) { + return False; + } + + return False; + + } + + /** + * Get the validation error message. + * + * @return string + */ + public function message() + { + return 'The ' . $this->attribute . ' must be a json with file of type: ' . implode(', ', $this->extensions) . ' encoded in base64.'; + } +}