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.
| Package | Affected versions | Patched versions |
|---|---|---|
grumpydictator/firefly-iiiPackagist | < 5.6.0 | 5.6.0 |
Affected products
1- Range: unspecified
Patches
106d319cd71b7Fix https://huntr.dev/bounties/d32f3d5a-0738-41ba-89de-34f2a772de76/
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- github.com/advisories/GHSA-gp6w-ccqv-p7qrghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2021-3729ghsaADVISORY
- github.com/firefly-iii/firefly-iii/commit/06d319cd71b7787aa919b3ba1ccf51e4ade67712ghsax_refsource_MISCWEB
- huntr.dev/bounties/d32f3d5a-0738-41ba-89de-34f2a772de76ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.