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

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

CVE-2021-3729

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.05.6.0

Affected products

1

Patches

1
06d319cd71b7

Fix https://huntr.dev/bounties/d32f3d5a-0738-41ba-89de-34f2a772de76/

https://github.com/firefly-iii/firefly-iiiJames ColeAug 20, 2021via ghsa
4 files changed · +125 64
  • app/Http/Controllers/CurrencyController.php+49 37 modified
    @@ -21,6 +21,7 @@
     declare(strict_types=1);
     
     namespace FireflyIII\Http\Controllers;
    +
     use FireflyIII\Exceptions\FireflyException;
     use FireflyIII\Http\Requests\CurrencyFormRequest;
     use FireflyIII\Models\TransactionCurrency;
    @@ -41,7 +42,7 @@
     class CurrencyController extends Controller
     {
         protected CurrencyRepositoryInterface $repository;
    -    protected UserRepositoryInterface $userRepository;
    +    protected UserRepositoryInterface     $userRepository;
     
         /**
          * CurrencyController constructor.
    @@ -54,7 +55,7 @@ public function __construct()
     
             $this->middleware(
                 function ($request, $next) {
    -                app('view')->share('title', (string) trans('firefly.currencies'));
    +                app('view')->share('title', (string)trans('firefly.currencies'));
                     app('view')->share('mainTitleIcon', 'fa-usd');
                     $this->repository     = app(CurrencyRepositoryInterface::class);
                     $this->userRepository = app(UserRepositoryInterface::class);
    @@ -63,6 +64,7 @@ function ($request, $next) {
                 }
             );
         }
    +
         /**
          * Create a currency.
          *
    @@ -75,13 +77,13 @@ public function create(Request $request)
             /** @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'))]));
    +            $request->session()->flash('error', (string)trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))]));
     
                 return redirect(route('currencies.index'));
             }
     
             $subTitleIcon = 'fa-plus';
    -        $subTitle     = (string) trans('firefly.create_currency');
    +        $subTitle     = (string)trans('firefly.create_currency');
     
             // put previous url in session if not redirect from store (not "create another").
             if (true !== session('currencies.create.fromStore')) {
    @@ -102,15 +104,23 @@ public function create(Request $request)
          *
          * @return RedirectResponse|Redirector
          */
    -    public function defaultCurrency(Request $request, TransactionCurrency $currency)
    +    public function defaultCurrency(Request $request)
         {
    -        app('preferences')->set('currencyPreference', $currency->code);
    -        app('preferences')->mark();
    -
    -        Log::channel('audit')->info(sprintf('Make %s the default currency.', $currency->code));
    -
    -        $this->repository->enable($currency);
    -        $request->session()->flash('success', (string) trans('firefly.new_default_currency', ['name' => $currency->name]));
    +        $currencyId = (int)$request->get('id');
    +        if ($currencyId > 0) {
    +            // valid currency?
    +            $currency = $this->repository->find($currencyId);
    +            if (null !== $currency) {
    +                app('preferences')->set('currencyPreference', $currency->code);
    +                app('preferences')->mark();
    +                Log::channel('audit')->info(sprintf('Make %s the default currency.', $currency->code));
    +
    +                $this->repository->enable($currency);
    +                $request->session()->flash('success', (string)trans('firefly.new_default_currency', ['name' => $currency->name]));
    +
    +                return redirect(route('currencies.index'));
    +            }
    +        }
     
             return redirect(route('currencies.index'));
         }
    @@ -129,7 +139,7 @@ public function delete(Request $request, TransactionCurrency $currency)
             $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'))]));
    +            $request->session()->flash('error', (string)trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))]));
                 Log::channel('audit')->info(sprintf('Tried to visit page to delete currency %s but is not site owner.', $currency->code));
     
                 return redirect(route('currencies.index'));
    @@ -138,7 +148,7 @@ public function delete(Request $request, TransactionCurrency $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)]);
    +            $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 visit page to delete currency %s but currency is in use.', $currency->code));
     
    @@ -147,7 +157,7 @@ public function delete(Request $request, TransactionCurrency $currency)
     
             // put previous url in session
             $this->rememberPreviousUri('currencies.delete.uri');
    -        $subTitle = (string) trans('form.delete_currency', ['name' => $currency->name]);
    +        $subTitle = (string)trans('form.delete_currency', ['name' => $currency->name]);
             Log::channel('audit')->info(sprintf('Visit page to delete currency %s.', $currency->code));
     
             return prefixView('currencies.delete', compact('currency', 'subTitle'));
    @@ -167,22 +177,22 @@ public function destroy(Request $request, TransactionCurrency $currency)
             $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'))]));
    +            $request->session()->flash('error', (string)trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))]));
                 Log::channel('audit')->info(sprintf('Tried to delete currency %s but is not site owner.', $currency->code));
     
                 return redirect(route('currencies.index'));
     
             }
     
             if ($this->repository->currencyInUse($currency)) {
    -            $request->session()->flash('error', (string) trans('firefly.cannot_delete_currency', ['name' => e($currency->name)]));
    +            $request->session()->flash('error', (string)trans('firefly.cannot_delete_currency', ['name' => e($currency->name)]));
                 Log::channel('audit')->info(sprintf('Tried to delete currency %s but is in use.', $currency->code));
     
                 return redirect(route('currencies.index'));
             }
     
             if ($this->repository->isFallbackCurrency($currency)) {
    -            $request->session()->flash('error', (string) trans('firefly.cannot_delete_fallback_currency', ['name' => e($currency->name)]));
    +            $request->session()->flash('error', (string)trans('firefly.cannot_delete_fallback_currency', ['name' => e($currency->name)]));
                 Log::channel('audit')->info(sprintf('Tried to delete currency %s but is FALLBACK.', $currency->code));
     
                 return redirect(route('currencies.index'));
    @@ -191,7 +201,7 @@ public function destroy(Request $request, TransactionCurrency $currency)
             Log::channel('audit')->info(sprintf('Deleted currency %s.', $currency->code));
             $this->repository->destroy($currency);
     
    -        $request->session()->flash('success', (string) trans('firefly.deleted_currency', ['name' => $currency->name]));
    +        $request->session()->flash('success', (string)trans('firefly.deleted_currency', ['name' => $currency->name]));
     
             return redirect($this->getPreviousUri('currencies.delete.uri'));
         }
    @@ -200,8 +210,8 @@ public function destroy(Request $request, TransactionCurrency $currency)
          * @param Request             $request
          * @param TransactionCurrency $currency
          *
    -     * @throws FireflyException
          * @return RedirectResponse|Redirector
    +     * @throws FireflyException
          */
         public function disableCurrency(Request $request, TransactionCurrency $currency)
         {
    @@ -211,7 +221,7 @@ public function disableCurrency(Request $request, TransactionCurrency $currency)
             $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'))]));
    +            $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'));
    @@ -221,7 +231,7 @@ public function disableCurrency(Request $request, TransactionCurrency $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)]);
    +            $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));
    @@ -245,10 +255,10 @@ public function disableCurrency(Request $request, TransactionCurrency $currency)
             }
     
             if ('EUR' === $currency->code) {
    -            session()->flash('warning', (string) trans('firefly.disable_EUR_side_effects'));
    +            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'));
         }
    @@ -267,21 +277,21 @@ public function edit(Request $request, TransactionCurrency $currency)
             $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'))]));
    +            $request->session()->flash('error', (string)trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))]));
                 Log::channel('audit')->info(sprintf('Tried to edit currency %s but is not owner.', $currency->code));
     
                 return redirect(route('currencies.index'));
     
             }
     
             $subTitleIcon     = 'fa-pencil';
    -        $subTitle         = (string) trans('breadcrumbs.edit_currency', ['name' => $currency->name]);
    +        $subTitle         = (string)trans('breadcrumbs.edit_currency', ['name' => $currency->name]);
             $currency->symbol = htmlentities($currency->symbol);
     
             // code to handle active-checkboxes
             $hasOldInput = null !== $request->old('_token');
             $preFilled   = [
    -            'enabled' => $hasOldInput ? (bool) $request->old('enabled') : $currency->enabled,
    +            'enabled' => $hasOldInput ? (bool)$request->old('enabled') : $currency->enabled,
             ];
     
             $request->session()->flash('preFilled', $preFilled);
    @@ -306,7 +316,7 @@ 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]));
    +        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'));
    @@ -323,8 +333,8 @@ public function index(Request $request)
         {
             /** @var User $user */
             $user       = auth()->user();
    -        $page       = 0 === (int) $request->get('page') ? 1 : (int) $request->get('page');
    -        $pageSize   = (int) app('preferences')->get('listPageSize', 50)->data;
    +        $page       = 0 === (int)$request->get('page') ? 1 : (int)$request->get('page');
    +        $pageSize   = (int)app('preferences')->get('listPageSize', 50)->data;
             $collection = $this->repository->getAll();
             $total      = $collection->count();
             $collection = $collection->slice(($page - 1) * $pageSize, $pageSize);
    @@ -334,12 +344,13 @@ public function index(Request $request)
             $defaultCurrency = $this->repository->getCurrencyByPreference(app('preferences')->get('currencyPreference', config('firefly.default_currency', 'EUR')));
             $isOwner         = true;
             if (!$this->userRepository->hasRole($user, 'owner')) {
    -            $request->session()->flash('info', (string) trans('firefly.ask_site_owner', ['owner' => config('firefly.site_owner')]));
    +            $request->session()->flash('info', (string)trans('firefly.ask_site_owner', ['owner' => config('firefly.site_owner')]));
                 $isOwner = false;
             }
     
             return prefixView('currencies.index', compact('currencies', 'defaultCurrency', 'isOwner'));
         }
    +
         /**
          * Store new currency.
          *
    @@ -367,15 +378,15 @@ public function store(CurrencyFormRequest $request)
             } catch (FireflyException $e) {
                 Log::error($e->getMessage());
                 Log::channel('audit')->info('Could not store (POST) currency without admin rights.', $data);
    -            $request->session()->flash('error', (string) trans('firefly.could_not_store_currency'));
    +            $request->session()->flash('error', (string)trans('firefly.could_not_store_currency'));
                 $currency = null;
             }
             $redirect = redirect($this->getPreviousUri('currencies.create.uri'));
     
             if (null !== $currency) {
    -            $request->session()->flash('success', (string) trans('firefly.created_currency', ['name' => $currency->name]));
    +            $request->session()->flash('success', (string)trans('firefly.created_currency', ['name' => $currency->name]));
                 Log::channel('audit')->info('Created (POST) currency.', $data);
    -            if (1 === (int) $request->get('create_another')) {
    +            if (1 === (int)$request->get('create_another')) {
     
                     $request->session()->put('currencies.create.fromStore', true);
     
    @@ -386,6 +397,7 @@ public function store(CurrencyFormRequest $request)
     
             return $redirect;
         }
    +
         /**
          * Updates a currency.
          *
    @@ -405,18 +417,18 @@ public function update(CurrencyFormRequest $request, TransactionCurrency $curren
             }
             if (!$this->userRepository->hasRole($user, 'owner')) {
     
    -            $request->session()->flash('error', (string) trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))]));
    +            $request->session()->flash('error', (string)trans('firefly.ask_site_owner', ['owner' => e(config('firefly.site_owner'))]));
                 Log::channel('audit')->info('Tried to update (POST) currency without admin rights.', $data);
     
                 return redirect(route('currencies.index'));
     
             }
             $currency = $this->repository->update($currency, $data);
             Log::channel('audit')->info('Updated (POST) currency.', $data);
    -        $request->session()->flash('success', (string) trans('firefly.updated_currency', ['name' => $currency->name]));
    +        $request->session()->flash('success', (string)trans('firefly.updated_currency', ['name' => $currency->name]));
             app('preferences')->mark();
     
    -        if (1 === (int) $request->get('return_to_edit')) {
    +        if (1 === (int)$request->get('return_to_edit')) {
     
                 $request->session()->put('currencies.edit.fromUpdate', true);
     
    
  • public/v1/js/ff/currencies/index.js+43 0 added
    @@ -0,0 +1,43 @@
    +/*
    + * index.js
    + * Copyright (c) 2019 james@firefly-iii.org
    + *
    + * This file is part of Firefly III (https://github.com/firefly-iii).
    + *
    + * This program is free software: you can redistribute it and/or modify
    + * it under the terms of the GNU Affero General Public License as
    + * published by the Free Software Foundation, either version 3 of the
    + * License, or (at your option) any later version.
    + *
    + * This program is distributed in the hope that it will be useful,
    + * but WITHOUT ANY WARRANTY; without even the implied warranty of
    + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    + * GNU Affero General Public License for more details.
    + *
    + * You should have received a copy of the GNU Affero General Public License
    + * along with this program.  If not, see <https://www.gnu.org/licenses/>.
    + */
    +
    +/**
    + *
    + */
    +$(function () {
    +    "use strict";
    +    $('.make_default').on('click', setDefaultCurrency);
    +
    +});
    +
    +function setDefaultCurrency(e) {
    +    var button = $(e.currentTarget);
    +    var currencyId = parseInt(button.data('id'));
    +
    +    $.post(makeDefaultUrl, {
    +        _token: token,
    +        id: currencyId
    +    }).done(function (data) {
    +        // lame but it works
    +        location.reload();
    +    }).fail(function () {
    +        console.error('I failed :(');
    +    });
    +}
    
  • resources/views/v1/currencies/index.twig+32 26 modified
    @@ -10,7 +10,7 @@
                 <div class="box">
                     <div class="box-header with-border">
                         <h3 class="box-title">{{ 'currencies'|_ }}</h3>
    -                        <a class="btn btn-success pull-right" href="{{ route('currencies.create') }}">{{ 'create_currency'|_ }}</a>
    +                    <a class="btn btn-success pull-right" href="{{ route('currencies.create') }}">{{ 'create_currency'|_ }}</a>
                     </div>
                     <div class="box-body">
                         <p class="text-info">
    @@ -40,46 +40,46 @@
                                         {% if isOwner %}
                                             <td>
                                                 <div class="btn-group btn-group-xs">
    -                                                <a class="btn btn-default" href="{{ route('currencies.edit',currency.id) }}"><span class="fa fa-fw fa-pencil"></span></a>
    -                                                <a class="btn btn-danger" href="{{ route('currencies.delete',currency.id) }}"><span class="fa fa-fw fa-trash"></span></a>
    +                                                <a class="btn btn-default" href="{{ route('currencies.edit',currency.id) }}"><span
    +                                                            class="fa fa-fw fa-pencil"></span></a>
    +                                                <a class="btn btn-danger" href="{{ route('currencies.delete',currency.id) }}"><span
    +                                                            class="fa fa-fw fa-trash"></span></a>
                                                 </div>
                                             </td>
                                         {% endif %}
                                         <td>
                                             {% if currency.enabled == false %}
    -                                            <span class="text-muted">
    +                                        <span class="text-muted">
                                             {% endif %}
    -                                        {{ currency.name }} ({{ currency.code }}) ({{ currency.symbol|raw }})
    +                                            {{ currency.name }} ({{ currency.code }}) ({{ currency.symbol|raw }})
                                             {% if currency.id == defaultCurrency.id %}
    -                                        <span class="label label-success" id="default-currency">{{ 'default_currency'|_ }}</span>
    +                                            <span class="label label-success" id="default-currency">{{ 'default_currency'|_ }}</span>
                                             {% endif %}
    -                                        {% if currency.enabled == false %}
    +                                            {% if currency.enabled == false %}
                                             </span>
    -                                        <br><small class="text-danger">{{ 'currency_is_disabled'|_ }}</small>
    +                                    <br><small class="text-danger">{{ 'currency_is_disabled'|_ }}</small>
                                             {% endif %}
                                         </td>
     
                                         <td>{{ currency.decimal_places }}</td>
                                         <td class="buttons">
                                             <div class="btn-group">
    -                                        {% if currency.id != defaultCurrency.id %}
    -                                            <a class="btn btn-default"
    -                                               href="{{ route('currencies.default',currency.id) }}">
    -                                                <span class="fa fa-fw fa-star"></span>
    -                                                {{ 'make_default_currency'|_ }}</a>
    -                                        {% endif %}
    -                                        {% if currency.enabled %}
    -                                            <a class="btn btn-default"
    -                                               href="{{ route('currencies.disable',currency.id) }}">
    -                                                <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) }}">
    -                                                <span class="fa fa-fw fa-check-square-o"></span>
    -                                                {{ 'enable_currency'|_ }}</a>
    -                                        {% endif %}
    +                                            {% if currency.id != defaultCurrency.id %}
    +                                                <button data-id="{{ currency.id }}" class="make_default btn btn-default"><span
    +                                                            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) }}">
    +                                                    <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) }}">
    +                                                    <span class="fa fa-fw fa-check-square-o"></span>
    +                                                    {{ 'enable_currency'|_ }}</a>
    +                                            {% endif %}
                                             </div>
                                         </td>
                                     </tr>
    @@ -98,3 +98,9 @@
             </div>
         </div>
     {% endblock %}
    +{% block scripts %}
    +    <script type="text/javascript" nonce="{{ JS_NONCE }}">
    +        var makeDefaultUrl = "{{ route('currencies.default') }}";
    +    </script>
    +    <script type="text/javascript" src="v1/js/ff/currencies/index.js?v={{ FF_VERSION }}" nonce="{{ JS_NONCE }}"></script>
    +{% endblock %}
    \ No newline at end of file
    
  • routes/web.php+1 1 modified
    @@ -330,7 +330,7 @@ static function () {
             Route::get('create', ['uses' => 'CurrencyController@create', 'as' => 'create']);
             Route::get('edit/{currency}', ['uses' => 'CurrencyController@edit', 'as' => 'edit']);
             Route::get('delete/{currency}', ['uses' => 'CurrencyController@delete', 'as' => 'delete']);
    -        Route::get('default/{currency}', ['uses' => 'CurrencyController@defaultCurrency', 'as' => 'default']);
    +        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']);
     
    

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

4

News mentions

0

No linked articles in our index yet.