CVE-2021-3819 (PHP, published 2021-09-27T13:15Z)
Description: value: firefly-iii is vulnerable to Cross-Site Request Forgery (CSRF)
Severity: MEDIUM (CVSS v3 base score: 8.8)
Weakness: CWE-352 \u2014 Cross-Site Request Forgery (CSRF)
The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.
Repository: https://github.com/firefly-iii/firefly-iii
Files changed:
app/Http/Controllers/CurrencyController.php
app/Http/Controllers/Transaction/CreateController.php
public/v1/js/ff/currencies/index.js
public/v1/js/ff/list/groups.js
public/v1/js/ff/transactions/show.js
resources/views/v1/currencies/index.twig
resources/views/v1/list/groups.twig
resources/views/v1/transactions/show.twig
routes/web.php
Commit message: Convert GET routes to POST.


Vulnerable code:
    public function disableCurrency(Request $request, TransactionCurrency $currency)
        app('preferences')->mark();
        /** @var User $user */
        $user = auth()->user();
        if (!$this->userRepository->hasRole($user, 'owner')) {
            $request->session()->flash('error', (string)trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))]));
            Log::channel('audit')->info(sprintf('Tried to disable currency %s but is not site owner.', $currency->code));
            return redirect(route('currencies.index'));
        }
        if ($this->repository->currencyInUse($currency)) {
            $location = $this->repository->currencyInUseAt($currency);
            $message  = (string)trans(sprintf('firefly.cannot_disable_currency_%s', $location), ['name' => e($currency->name)]);
            $request->session()->flash('error', $message);
            Log::channel('audit')->info(sprintf('Tried to disable currency %s but is in use.', $currency->code));
            return redirect(route('currencies.index'));
        }
        $this->repository->disable($currency);
        Log::channel('audit')->info(sprintf('Disabled currency %s.', $currency->code));
        // if no currencies are enabled, enable the first one in the DB (usually the EUR)
        if (0 === $this->repository->get()->count()) {
            /** @var TransactionCurrency $first */
            $first = $this->repository->getAll()->first();
            if (null === $first) {
                throw new FireflyException('No currencies found.');
            }
            Log::channel('audit')->info(sprintf('Auto-enabled currency %s.', $first->code));
            $this->repository->enable($first);
            app('preferences')->set('currencyPreference', $first->code);
            app('preferences')->mark();
        }
        if ('EUR' === $currency->code) {
            session()->flash('warning', (string)trans('firefly.disable_EUR_side_effects'));
        }
        session()->flash('success', (string)trans('firefly.currency_is_now_disabled', ['name' => $currency->name]));
    public function enableCurrency(TransactionCurrency $currency)
        app('preferences')->mark();
        $this->repository->enable($currency);
        session()->flash('success', (string)trans('firefly.currency_is_now_enabled', ['name' => $currency->name]));
        Log::channel('audit')->info(sprintf('Enabled currency %s.', $currency->code));
use Illuminate\Http\RedirectResponse;
use Illuminate\Routing\Redirector;
            static function ($request, $next) {
     * @param TransactionGroup $group
     * @return RedirectResponse|Redirector
    public function cloneGroup(TransactionGroup $group)
        /** @var GroupCloneService $service */
        $service  = app(GroupCloneService::class);
        $newGroup = $service->cloneGroup($group);
        // event!
        event(new StoredTransactionGroup($newGroup));
        app('preferences')->mark();
        $title = $newGroup->title ?? $newGroup->transactionJournals->first()->description;
        $link  = route('transactions.show', [$newGroup->id]);
        session()->flash('success', trans('firefly.stored_journal', ['description' => $title]));
        session()->flash('success_url', $link);
        return redirect(route('transactions.show', [$newGroup->id]));
}
                                                <a class="btn btn-default"
                                                   href="{{ route('currencies.disable',currency.id) }}">
                                                <a class="btn btn-default"
                                                   href="{{ route('currencies.enable',currency.id) }}">
                            <li><a href="{{ route('transactions.clone', [group.id]) }}"><span
                                <li><a href="{{ route('transactions.clone', [group.id]) }}"><span
                                <!-- since 5.1.0 -->
                                <li><a href="{{ route('transactions.clone', [transactionGroup.id]) }}"><span class="fa fa-copy"></span> {{ 'clone'|_ }}</a></li>
                                    <!-- since 5.1.0 -->
                                    <li><a href="{{ route('transactions.clone', [transactionGroup.id]) }}"><span class="fa fa-copy"></span> {{ 'clone'|_ }}</a></li>

        Route::get('enable/{currency}', ['uses' => 'CurrencyController@enableCurrency', 'as' => 'enable']);
        Route::get('disable/{currency}', ['uses' => 'CurrencyController@disableCurrency', 'as' => 'disable']);
        Route::get('clone/{transactionGroup}', ['uses' => 'Transaction\CreateController@cloneGroup', 'as' => 'clone']);

Fixed code:
    public function disableCurrency(Request $request)
        $currencyId = (int)$request->get('id');
        if ($currencyId > 0) {
            // valid currency?
            $currency = $this->repository->find($currencyId);
            if (null !== $currency) {
                app('preferences')->mark();
                /** @var User $user */
                $user = auth()->user();
                if (!$this->userRepository->hasRole($user, 'owner')) {
                    $request->session()->flash('error', (string)trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))]));
                    Log::channel('audit')->info(sprintf('Tried to disable currency %s but is not site owner.', $currency->code));
                    return redirect(route('currencies.index'));
                }
                if ($this->repository->currencyInUse($currency)) {
                    $location = $this->repository->currencyInUseAt($currency);
                    $message  = (string)trans(sprintf('firefly.cannot_disable_currency_%s', $location), ['name' => e($currency->name)]);
                    $request->session()->flash('error', $message);
                    Log::channel('audit')->info(sprintf('Tried to disable currency %s but is in use.', $currency->code));
                    return redirect(route('currencies.index'));
                }
                $this->repository->disable($currency);
                Log::channel('audit')->info(sprintf('Disabled currency %s.', $currency->code));
                // if no currencies are enabled, enable the first one in the DB (usually the EUR)
                if (0 === $this->repository->get()->count()) {
                    /** @var TransactionCurrency $first */
                    $first = $this->repository->getAll()->first();
                    if (null === $first) {
                        throw new FireflyException('No currencies found.');
                    }
                    Log::channel('audit')->info(sprintf('Auto-enabled currency %s.', $first->code));
                    $this->repository->enable($first);
                    app('preferences')->set('currencyPreference', $first->code);
                    app('preferences')->mark();
                }
                if ('EUR' === $currency->code) {
                    session()->flash('warning', (string)trans('firefly.disable_EUR_side_effects'));
                }
                session()->flash('success', (string)trans('firefly.currency_is_now_disabled', ['name' => $currency->name]));
            }
        }
    public function enableCurrency(Request $request)
        $currencyId = (int)$request->get('id');
        if ($currencyId > 0) {
            // valid currency?
            $currency = $this->repository->find($currencyId);
            if (null !== $currency) {
                app('preferences')->mark();
                $this->repository->enable($currency);
                session()->flash('success', (string)trans('firefly.currency_is_now_enabled', ['name' => $currency->name]));
                Log::channel('audit')->info(sprintf('Enabled currency %s.', $currency->code));
            }
        }
use FireflyIII\Repositories\TransactionGroup\TransactionGroupRepositoryInterface;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
    private TransactionGroupRepositoryInterface $repository;

            function ($request, $next) {
                $this->repository = app(TransactionGroupRepositoryInterface::class);
     * @param Request $request
     * @return JsonResponse
    public function cloneGroup(Request $request): JsonResponse
        $groupId = (int)$request->get('id');
        if (0 !== $groupId) {
            $group = $this->repository->find($groupId);
            if (null !== $group) {
                /** @var GroupCloneService $service */
                $service  = app(GroupCloneService::class);
                $newGroup = $service->cloneGroup($group);
                // event!
                event(new StoredTransactionGroup($newGroup));
                app('preferences')->mark();
                $title = $newGroup->title ?? $newGroup->transactionJournals->first()->description;
                $link  = route('transactions.show', [$newGroup->id]);
                session()->flash('success', trans('firefly.stored_journal', ['description' => $title]));
                session()->flash('success_url', $link);
                return response()->json(['redirect' => route('transactions.show', [$newGroup->id])]);
            }
        }
        return response()->json(['redirect' => route('transactions.show', [$groupId])]);
    $('.enable-currency').on('click', enableCurrency);
    $('.disable-currency').on('click', disableCurrency);
    return false;
}

function enableCurrency(e) {
    var button = $(e.currentTarget);
    var currencyId = parseInt(button.data('id'));

    $.post(enableCurrencyUrl, {
        _token: token,
        id: currencyId
    }).done(function (data) {
        // lame but it works
        location.reload();
    }).fail(function () {
        console.error('I failed :(');
    });
    return false;
}

function disableCurrency(e) {
    var button = $(e.currentTarget);
    var currencyId = parseInt(button.data('id'));

    $.post(disableCurrencyUrl, {
        _token: token,
        id: currencyId
    }).done(function (data) {
        // lame but it works
        location.reload();
    }).fail(function () {
        console.error('I failed :(');
    });
    return false;
    $('.clone-transaction').click(cloneTransaction);
}
function cloneTransaction(e) {
    var button = $(e.currentTarget);
    var groupId = parseInt(button.data('id'));

    $.post(cloneGroupUrl, {
        _token: token,
        id: groupId
    }).done(function (data) {
        // lame but it works
        location.href = data.redirect;
    }).fail(function () {
        console.error('I failed :(');
    });
    return false;
}
    $('.clone-transaction').click(cloneTransaction);

function cloneTransaction(e) {
    var button = $(e.currentTarget);
    var groupId = parseInt(button.data('id'));

    $.post(cloneGroupUrl, {
        _token: token,
        id: groupId
    }).done(function (data) {
        // lame but it works
        location.href = data.redirect;
    }).fail(function () {
        console.error('I failed :(');
    });
    return false;
}
                                                <a class="btn btn-default disable-currency" data-id="{{ currency.id }}"
                                                   href="#">
                                                <a class="btn btn-default enable-currency" data-id="{{ currency.id }}"
                                                   href="#">
        var disableCurrencyUrl = "{{ route('currencies.disable') }}";
        var enableCurrencyUrl = "{{ route('currencies.enable') }}";
                            <li><a href="#" data-id="{{ group.id }}" class="clone-transaction"><span
                                <li><a href="#" data-id="{{ group.id }}" class="clone-transaction"><span
<script type="text/javascript" nonce="{{ JS_NONCE }}">
    var cloneGroupUrl = '{{ route('transactions.clone') }}';
</script>
                                <li><a href="#" class="clone-transaction" data-id="{{ transactionGroup.id }}"><span class="fa fa-copy"></span> {{ 'clone'|_ }}</a></li>
                                    <li><a href="#" data-id="{{ transactionGroup.id }}" class="clone-transaction"><span class="fa fa-copy"></span> {{ 'clone'|_ }}</a></li>
        var cloneGroupUrl = '{{ route('transactions.clone') }}';
        Route::post('enable', ['uses' => 'CurrencyController@enableCurrency', 'as' => 'enable']);
        Route::post('disable', ['uses' => 'CurrencyController@disableCurrency', 'as' => 'disable']);
        Route::post('clone', ['uses' => 'Transaction\CreateController@cloneGroup', 'as' => 'clone']);