VYPR
Moderate severityNVD Advisory· Published Sep 27, 2021· Updated Aug 3, 2024

Cross-Site Request Forgery (CSRF) in firefly-iii/firefly-iii

CVE-2021-3819

Description

firefly-iii is vulnerable to Cross-Site Request Forgery (CSRF)

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
grumpydictator/firefly-iiiPackagist
< 5.6.15.6.1

Affected products

1

Patches

1
578f350498b7

Convert GET routes to POST.

https://github.com/firefly-iii/firefly-iiiJames ColeSep 20, 2021via ghsa
9 files changed · +167 71
  • app/Http/Controllers/CurrencyController.php+53 39 modified
    @@ -213,52 +213,59 @@ public function destroy(Request $request, TransactionCurrency $currency)
          * @return RedirectResponse|Redirector
          * @throws FireflyException
          */
    -    public function disableCurrency(Request $request, TransactionCurrency $currency)
    +    public function disableCurrency(Request $request)
         {
    -        app('preferences')->mark();
    +        $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')) {
    +                /** @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));
    +                    $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'));
    +                    return redirect(route('currencies.index'));
     
    -        }
    +                }
     
    -        if ($this->repository->currencyInUse($currency)) {
    +                if ($this->repository->currencyInUse($currency)) {
     
    -            $location = $this->repository->currencyInUseAt($currency);
    -            $message  = (string)trans(sprintf('firefly.cannot_disable_currency_%s', $location), ['name' => e($currency->name)]);
    +                    $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));
    +                    $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'));
    -        }
    +                    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();
    -        }
    +                $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'));
    -        }
    +                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]));
    +                session()->flash('success', (string)trans('firefly.currency_is_now_disabled', ['name' => $currency->name]));
    +            }
    +        }
     
             return redirect(route('currencies.index'));
         }
    @@ -311,13 +318,20 @@ public function edit(Request $request, TransactionCurrency $currency)
          *
          * @return RedirectResponse|Redirector
          */
    -    public function enableCurrency(TransactionCurrency $currency)
    +    public function enableCurrency(Request $request)
         {
    -        app('preferences')->mark();
    +        $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));
    +                $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));
    +            }
    +        }
     
             return redirect(route('currencies.index'));
         }
    
  • app/Http/Controllers/Transaction/CreateController.php+28 17 modified
    @@ -28,17 +28,20 @@
     use FireflyIII\Http\Controllers\Controller;
     use FireflyIII\Models\TransactionGroup;
     use FireflyIII\Repositories\Account\AccountRepositoryInterface;
    +use FireflyIII\Repositories\TransactionGroup\TransactionGroupRepositoryInterface;
     use FireflyIII\Services\Internal\Update\GroupCloneService;
     use Illuminate\Contracts\View\Factory;
     use Illuminate\Contracts\View\View;
    -use Illuminate\Http\RedirectResponse;
    -use Illuminate\Routing\Redirector;
    +use Illuminate\Http\JsonResponse;
    +use Illuminate\Http\Request;
     
     /**
      * Class CreateController
      */
     class CreateController extends Controller
     {
    +    private TransactionGroupRepositoryInterface $repository;
    +
         /**
          * CreateController constructor.
          *
    @@ -49,38 +52,46 @@ public function __construct()
             parent::__construct();
     
             $this->middleware(
    -            static function ($request, $next) {
    +            function ($request, $next) {
                     app('view')->share('title', (string)trans('firefly.transactions'));
                     app('view')->share('mainTitleIcon', 'fa-exchange');
    +                $this->repository = app(TransactionGroupRepositoryInterface::class);
     
                     return $next($request);
                 }
             );
         }
     
         /**
    -     * @param TransactionGroup $group
    +     * @param Request $request
          *
    -     * @return RedirectResponse|Redirector
    +     * @return JsonResponse
          */
    -    public function cloneGroup(TransactionGroup $group)
    +    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);
     
    -        /** @var GroupCloneService $service */
    -        $service  = app(GroupCloneService::class);
    -        $newGroup = $service->cloneGroup($group);
    +                // event!
    +                event(new StoredTransactionGroup($newGroup));
     
    -        // event!
    -        event(new StoredTransactionGroup($newGroup));
    +                app('preferences')->mark();
     
    -        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);
     
    -        $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 redirect(route('transactions.show', [$newGroup->id]));
    +        return response()->json(['redirect' => route('transactions.show', [$groupId])]);
         }
     
         /**
    
  • public/v1/js/ff/currencies/index.js+35 0 modified
    @@ -25,6 +25,8 @@ $(function () {
         "use strict";
         $('.make_default').on('click', setDefaultCurrency);
     
    +    $('.enable-currency').on('click', enableCurrency);
    +    $('.disable-currency').on('click', disableCurrency);
     });
     
     function setDefaultCurrency(e) {
    @@ -40,4 +42,37 @@ function setDefaultCurrency(e) {
         }).fail(function () {
             console.error('I failed :(');
         });
    +    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;
     }
    
  • public/v1/js/ff/list/groups.js+17 1 modified
    @@ -23,6 +23,7 @@ var count = 0;
     $(document).ready(function () {
         updateListButtons();
         addSort();
    +    $('.clone-transaction').click(cloneTransaction);
     });
     
     var fixHelper = function (e, tr) {
    @@ -206,4 +207,19 @@ function updateActionButtons() {
         if (0 === count) {
             $('.action-menu').hide();
         }
    -}
    \ No newline at end of file
    +}
    +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;
    +}
    
  • public/v1/js/ff/transactions/show.js+17 0 modified
    @@ -23,6 +23,7 @@
     $(function () {
         "use strict";
         $('.link-modal').click(getLinkModal);
    +    $('.clone-transaction').click(cloneTransaction);
         $('#linkJournalModal').on('shown.bs.modal', function () {
             makeAutoComplete();
         })
    @@ -80,3 +81,19 @@ function selectedJournal(event, journal) {
         $('#selected-journal').html('<a href="' + groupURI.replace('%GROUP%', journal.transaction_group_id) + '">' + journal.description + '</a>').show();
         $('input[name="opposing"]').val(journal.id);
     }
    +
    +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;
    +}
    \ No newline at end of file
    
  • resources/views/v1/currencies/index.twig+6 4 modified
    @@ -69,14 +69,14 @@
                                                                 class="fa fa-fw fa-star"></span> {{ 'make_default_currency'|_ }}</button>
                                                 {% endif %}
                                                 {% if currency.enabled %}
    -                                                <a class="btn btn-default"
    -                                                   href="{{ route('currencies.disable',currency.id) }}">
    +                                                <a class="btn btn-default disable-currency" data-id="{{ currency.id }}"
    +                                                   href="#">
                                                         <span class="fa fa-fw fa-square-o"></span>
                                                         {{ 'disable_currency'|_ }}</a>
                                                 {% endif %}
                                                 {% if not currency.enabled %}
    -                                                <a class="btn btn-default"
    -                                                   href="{{ route('currencies.enable',currency.id) }}">
    +                                                <a class="btn btn-default enable-currency" data-id="{{ currency.id }}"
    +                                                   href="#">
                                                         <span class="fa fa-fw fa-check-square-o"></span>
                                                         {{ 'enable_currency'|_ }}</a>
                                                 {% endif %}
    @@ -101,6 +101,8 @@
     {% block scripts %}
         <script type="text/javascript" nonce="{{ JS_NONCE }}">
             var makeDefaultUrl = "{{ route('currencies.default') }}";
    +        var disableCurrencyUrl = "{{ route('currencies.disable') }}";
    +        var enableCurrencyUrl = "{{ route('currencies.enable') }}";
         </script>
         <script type="text/javascript" src="v1/js/ff/currencies/index.js?v={{ FF_VERSION }}" nonce="{{ JS_NONCE }}"></script>
     {% endblock %}
    
  • resources/views/v1/list/groups.twig+5 2 modified
    @@ -85,7 +85,7 @@
                                                 class="fa fa-fw fa-pencil"></span> {{ 'edit'|_ }}</a></li>
                                 <li><a href="{{ route('transactions.delete', [group.id]) }}"><span
                                                 class="fa fa-fw fa-trash"></span> {{ 'delete'|_ }}</a></li>
    -                            <li><a href="{{ route('transactions.clone', [group.id]) }}"><span
    +                            <li><a href="#" data-id="{{ group.id }}" class="clone-transaction"><span
                                                 class="fa fa-copy fa-fw"></span> {{ 'clone'|_ }}</a></li>
                             </ul>
                         </div>
    @@ -249,7 +249,7 @@
                                                     class="fa fa-fw fa-pencil"></span> {{ 'edit'|_ }}</a></li>
                                     <li><a href="{{ route('transactions.delete', [group.id]) }}"><span
                                                     class="fa fa-fw fa-trash"></span> {{ 'delete'|_ }}</a></li>
    -                                <li><a href="{{ route('transactions.clone', [group.id]) }}"><span
    +                                <li><a href="#" data-id="{{ group.id }}" class="clone-transaction"><span
                                                     class="fa fa-copy fa-fw"></span> {{ 'clone'|_ }}</a></li>
                                     <li>
                                         <a href="{{ route('rules.create-from-journal', [transaction.transaction_journal_id]) }}"><span
    @@ -309,3 +309,6 @@
         </tr>
         </tfoot>
     </table>
    +<script type="text/javascript" nonce="{{ JS_NONCE }}">
    +    var cloneGroupUrl = '{{ route('transactions.clone') }}';
    +</script>
    
  • resources/views/v1/transactions/show.twig+3 5 modified
    @@ -35,9 +35,8 @@
     
                                 {#  clone #}
                                 {% if groupArray.transactions[0].type != 'opening balance' and groupArray.transactions[0].type != 'reconciliation' %}
    -                                <!-- since 5.1.0 -->
                                     <li role="separator" class="divider"></li>
    -                                <li><a href="{{ route('transactions.clone', [transactionGroup.id]) }}"><span class="fa fa-copy"></span> {{ 'clone'|_ }}</a></li>
    +                                <li><a href="#" class="clone-transaction" data-id="{{ transactionGroup.id }}"><span class="fa fa-copy"></span> {{ 'clone'|_ }}</a></li>
                                 {% endif %}
     
                             </ul>
    @@ -208,9 +207,8 @@
     
                                     {#  clone #}
                                     {% if groupArray.transactions[0].type != 'opening balance' and groupArray.transactions[0].type != 'reconciliation' %}
    -                                    <!-- since 5.1.0 -->
                                         <li role="separator" class="divider"></li>
    -                                    <li><a href="{{ route('transactions.clone', [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>
                                     {% endif %}
     
                                     <li><a href="#" class="link-modal" data-journal="{{ journal.transaction_journal_id }}"><span class="fa fa-fw fa-link"></span>{{ 'link_transaction'|_ }}</a></li>
    @@ -219,7 +217,6 @@
                                     <li><a href="{{ route('recurring.create-from-journal', [journal.transaction_journal_id]) }}"><span class="fa fa-fw fa-paint-brush"></span>{{ 'create_recurring_from_transaction'|_ }}</a></li>
                                 </ul>
                             </div>
    -
                         </div>
                         <div class="box-body no-padding">
                             <table class="table">
    @@ -425,6 +422,7 @@
             var modalDialogURI = '{{ route('transactions.link.modal', ['%JOURNAL%']) }}';
             var acURI = '{{ route('api.v1.autocomplete.transactions-with-id') }}';
             var groupURI = '{{ route('transactions.show',['%GROUP%']) }}';
    +        var cloneGroupUrl = '{{ route('transactions.clone') }}';
         </script>
         <script type="text/javascript" src="v1/js/lib/typeahead/typeahead.bundle.min.js?v={{ FF_VERSION }}" nonce="{{ JS_NONCE }}"></script>
         <script type="text/javascript" src="v1/js/ff/transactions/show.js?v={{ FF_VERSION }}" nonce="{{ JS_NONCE }}"></script>
    
  • routes/web.php+3 3 modified
    @@ -338,8 +338,8 @@ static function () {
             Route::get('edit/{currency}', ['uses' => 'CurrencyController@edit', 'as' => 'edit']);
             Route::get('delete/{currency}', ['uses' => 'CurrencyController@delete', 'as' => 'delete']);
             Route::post('default', ['uses' => 'CurrencyController@defaultCurrency', 'as' => 'default']);
    -        Route::get('enable/{currency}', ['uses' => 'CurrencyController@enableCurrency', 'as' => 'enable']);
    -        Route::get('disable/{currency}', ['uses' => 'CurrencyController@disableCurrency', 'as' => 'disable']);
    +        Route::post('enable', ['uses' => 'CurrencyController@enableCurrency', 'as' => 'enable']);
    +        Route::post('disable', ['uses' => 'CurrencyController@disableCurrency', 'as' => 'disable']);
     
             Route::post('store', ['uses' => 'CurrencyController@store', 'as' => 'store']);
             Route::post('update/{currency}', ['uses' => 'CurrencyController@update', 'as' => 'update']);
    @@ -1012,7 +1012,7 @@ static function () {
             Route::post('store', ['uses' => 'Transaction\CreateController@store', 'as' => 'store']);
     
             // clone group
    -        Route::get('clone/{transactionGroup}', ['uses' => 'Transaction\CreateController@cloneGroup', 'as' => 'clone']);
    +        Route::post('clone', ['uses' => 'Transaction\CreateController@cloneGroup', 'as' => 'clone']);
     
             // edit group
             Route::get('edit/{transactionGroup}', ['uses' => 'Transaction\EditController@edit', 'as' => 'edit']);
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

3

News mentions

0

No linked articles in our index yet.