CVE-2022-0372 (PHP, published 2022-01-27T08:15Z)
Description: value: Cross-site Scripting (XSS) - Stored in Packagist bytefury/crater prior to 6.0.2.
Severity: LOW (CVSS v3 base score: 5.4)
Weakness: CWE-79 \u2014 Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Repository: https://github.com/crater-invoice/crater
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 message: Unrestricted php file upload fix (#681)

https://huntr.dev/bounties/d7453360-baca-4e56-985f-481275fa38db/

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.';
    }
}