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.
| Package | Affected versions | Patched versions |
|---|---|---|
grumpydictator/firefly-iiiPackagist | < 5.6.1 | 5.6.1 |
Affected products
1- Range: unspecified
Patches
1578f350498b7Convert GET routes to POST.
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- github.com/advisories/GHSA-356r-77q8-f64fghsaADVISORY
- github.com/firefly-iii/firefly-iii/commit/578f350498b75f31d321c78a608c7f7b3b7b07e9ghsax_refsource_MISCWEB
- huntr.dev/bounties/da82f7b6-4ffc-4109-87a4-a2a790bd44e5ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.