CVE-2021-4080: Unrestricted Upload of File with Dangerous Type (CWE-434)
value: crater is vulnerable to Unrestricted Upload of File with Dangerous Type
CWE description: The product allows the upload or transfer of dangerous file types that are automatically processed within its environment.
CVSS v3 base score: 8.8
Published: 2022-01-12T14:15Z
Repository: https://github.com/crater-invoice/crater
Language: PHP
Files changed: app/Http/Controllers/V1/Admin/Expense/ExpensesController.php, app/Http/Controllers/V1/Admin/Expense/UploadReceiptController.php, app/Http/Controllers/V1/Admin/Settings/CompanyController.php, app/Http/Requests/AvatarRequest.php, app/Http/Requests/CompanyLogoRequest.php, app/Http/Requests/ExpenseRequest.php, app/Rules/Base64Mime.php
Commit date: 2021-12-29 18:03:20 +0530
Commit message: Unrestricted php file upload fix (#681)

https://huntr.dev/bounties/d7453360-baca-4e56-985f-481275fa38db/
Diff: diff --git a/app/Http/Controllers/V1/Admin/Expense/ExpensesController.php b/app/Http/Controllers/V1/Admin/Expense/ExpensesController.php
index bcb9400..9027a1f 100644
--- a/app/Http/Controllers/V1/Admin/Expense/ExpensesController.php
+++ b/app/Http/Controllers/V1/Admin/Expense/ExpensesController.php
@@ -37,11 +37,11 @@ class ExpensesController extends Controller
     }
 
     /**
      * 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)
     {
         $this->authorize('create', Expense::class);
@@ -65,11 +65,11 @@ class ExpensesController extends Controller
     }
 
     /**
      * 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
      */
     public function update(ExpenseRequest $request, Expense $expense)
     {
diff --git a/app/Http/Controllers/V1/Admin/Expense/UploadReceiptController.php b/app/Http/Controllers/V1/Admin/Expense/UploadReceiptController.php
index cc7d613..9c58d32 100644
--- a/app/Http/Controllers/V1/Admin/Expense/UploadReceiptController.php
+++ b/app/Http/Controllers/V1/Admin/Expense/UploadReceiptController.php
@@ -3,21 +3,22 @@
 namespace Crater\Http\Controllers\V1\Admin\Expense;
 
 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);
 
         $data = json_decode($request->attachment_receipt);
 
diff --git a/app/Http/Controllers/V1/Admin/Settings/CompanyController.php b/app/Http/Controllers/V1/Admin/Settings/CompanyController.php
index f594e13..e299c76 100644
--- a/app/Http/Controllers/V1/Admin/Settings/CompanyController.php
+++ b/app/Http/Controllers/V1/Admin/Settings/CompanyController.php
@@ -7,10 +7,12 @@ use Crater\Http\Requests\CompanyRequest;
 use Crater\Http\Requests\ProfileRequest;
 use Crater\Http\Resources\CompanyResource;
 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
 {
     /**
      * Retrive the Admin account.
@@ -56,14 +58,14 @@ class CompanyController extends Controller
     }
 
     /**
      * 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'));
 
         $this->authorize('manage company', $company);
 
@@ -87,14 +89,14 @@ class CompanyController extends Controller
     }
 
     /**
      * 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();
 
         if ($user && $request->hasFile('admin_avatar')) {
             $user->clearMediaCollection('admin_avatar');
diff --git a/app/Http/Requests/AvatarRequest.php b/app/Http/Requests/AvatarRequest.php
new file mode 100644
index 0000000..832a0ed
--- /dev/null
+++ b/app/Http/Requests/AvatarRequest.php
@@ -0,0 +1,40 @@
+<?php
+
+namespace Crater\Http\Requests;
+
+use Crater\Rules\Base64Mime;
+use Illuminate\Foundation\Http\FormRequest;
+
+class AvatarRequest extends FormRequest
+{
+    /**
+     * Determine if the user is authorized to make this request.
+     *
+     * @return bool
+     */
+    public function authorize()
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     *
+     * @return array
+     */
+    public function rules()
+    {
+        return [
+            'admin_avatar' => [
+                '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 0000000..3ff0c68
--- /dev/null
+++ b/app/Http/Requests/CompanyLogoRequest.php
@@ -0,0 +1,34 @@
+<?php
+
+namespace Crater\Http\Requests;
+
+use Crater\Rules\Base64Mime;
+use Illuminate\Foundation\Http\FormRequest;
+
+class CompanyLogoRequest extends FormRequest
+{
+    /**
+     * Determine if the user is authorized to make this request.
+     *
+     * @return bool
+     */
+    public function authorize()
+    {
+        return true;
+    }
+
+    /**
+     * Get the validation rules that apply to the request.
+     *
+     * @return array
+     */
+    public function rules()
+    {
+        return [
+            'company_logo' => [
+                'nullable',
+                new Base64Mime(['gif', 'jpg', 'png'])
+            ]
+        ];
+    }
+}
diff --git a/app/Http/Requests/ExpenseRequest.php b/app/Http/Requests/ExpenseRequest.php
index b74f9be..a8baa72 100644
--- a/app/Http/Requests/ExpenseRequest.php
+++ b/app/Http/Requests/ExpenseRequest.php
@@ -49,10 +49,16 @@ class ExpenseRequest extends FormRequest
                 'nullable',
             ],
             'currency_id' => [
                 'required'
             ],
+            'attachment_receipt' => [
+                'nullable',
+                'file',
+                'mimes:jpg,png,pdf,doc,docx,xls,xlsx,ppt,pptx',
+                'max:20000'
+            ]
         ];
 
         if ($companyCurrency && $this->currency_id) {
             if ($companyCurrency !== $this->currency_id) {
                 $rules['exchange_rate'] = [
diff --git a/app/Rules/Base64Mime.php b/app/Rules/Base64Mime.php
new file mode 100644
index 0000000..bb020e7
--- /dev/null
+++ b/app/Rules/Base64Mime.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Crater\Rules;
+
+use Illuminate\Contracts\Validation\Rule;
+
+class Base64Mime implements Rule
+{
+    private $attribute;
+    private $extensions;
+
+    /**
+     * Create a new rule instance.
+     *
+     * @return void
+     */
+    public function __construct(array $extensions)
+    {
+        $this->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.';
+    }
+}

Vulnerable code:      * @param  \Illuminate\Http\Request $request
     * @param  \Illuminate\Http\Request $request
     * @param  \Illuminate\Http\Request $request
    public function __invoke(Request $request, Expense $expense)
     * @param  \Illuminate\Http\Request $request
    public function uploadCompanyLogo(Request $request)
     * @param  \Illuminate\Http\Request $request
    public function uploadAvatar(Request $request)
Fixed code:      * @param  \Crater\Http\Requests\ExpenseRequest $request
     * @param  \Crater\Http\Requests\ExpenseRequest $request
use Crater\Http\Requests\ExpenseRequest;
     * @param  \Crater\Http\Requests\ExpenseRequest $request
    public function __invoke(ExpenseRequest $request, Expense $expense)
use Crater\Http\Requests\AvatarRequest;
use Crater\Http\Requests\CompanyLogoRequest;
     * @param  \Crater\Http\Requests\CompanyLogoRequest $request
    public function uploadCompanyLogo(CompanyLogoRequest $request)
     * @param  \Crater\Http\Requests\AvatarRequest $request
    public function uploadAvatar(AvatarRequest $request)
<?php

namespace Crater\Http\Requests;

use Crater\Rules\Base64Mime;
use Illuminate\Foundation\Http\FormRequest;

class AvatarRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'admin_avatar' => [
                'nullable',
                'file',
                'mimes:gif,jpg,png',
                'max:20000'
            ],
            'avatar' => [
                'nullable',
                new Base64Mime(['gif', 'jpg', 'png'])
            ]
        ];
    }
}
<?php

namespace Crater\Http\Requests;

use Crater\Rules\Base64Mime;
use Illuminate\Foundation\Http\FormRequest;

class CompanyLogoRequest extends FormRequest
{
    /**
     * Determine if the user is authorized to make this request.
     *
     * @return bool
     */
    public function authorize()
    {
        return true;
    }

    /**
     * Get the validation rules that apply to the request.
     *
     * @return array
     */
    public function rules()
    {
        return [
            'company_logo' => [
                'nullable',
                new Base64Mime(['gif', 'jpg', 'png'])
            ]
        ];
    }
}
            'attachment_receipt' => [
                'nullable',
                'file',
                'mimes:jpg,png,pdf,doc,docx,xls,xlsx,ppt,pptx',
                'max:20000'
            ]
<?php

namespace Crater\Rules;

use Illuminate\Contracts\Validation\Rule;

class Base64Mime implements Rule
{
    private $attribute;
    private $extensions;

    /**
     * Create a new rule instance.
     *
     * @return void
     */
    public function __construct(array $extensions)
    {
        $this->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.';
    }
}