Moderate severityNVD Advisory· Published Oct 27, 2021· Updated Aug 3, 2024
Cross-Site Request Forgery (CSRF) in firefly-iii/firefly-iii
CVE-2021-3900
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.2 | — |
Affected products
1- Range: unspecified
Patches
1c2c8c42ef319Catch CSRF issues
6 files changed · +76 −46
app/Http/Controllers/Rule/CreateController.php+9 −6 modified@@ -34,6 +34,7 @@ use FireflyIII\Support\Http\Controllers\RuleManagement; use FireflyIII\Support\Search\SearchInterface; use Illuminate\Contracts\View\Factory; +use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Routing\Redirector; @@ -237,15 +238,17 @@ public function createFromJournal(Request $request, TransactionJournal $journal) /** * @param Rule $rule * - * @return RedirectResponse + * @return JsonResponse */ - public function duplicate(Rule $rule): RedirectResponse + public function duplicate(Request $request): JsonResponse { - $newRule = $this->ruleRepos->duplicate($rule); - - session()->flash('success', trans('firefly.duplicated_rule', ['title' => $rule->title, 'newTitle' => $newRule->title])); + $ruleId = (int)$request->get('id'); + $rule = $this->ruleRepos->find($ruleId); + if (null !== $rule) { + $this->ruleRepos->duplicate($rule); + } - return redirect(route('rules.index')); + return new JsonResponse(['OK']); } /**
app/Http/Controllers/RuleGroup/EditController.php+26 −30 modified@@ -28,6 +28,7 @@ use FireflyIII\Models\RuleGroup; use FireflyIII\Repositories\RuleGroup\RuleGroupRepositoryInterface; use Illuminate\Contracts\View\Factory; +use Illuminate\Http\JsonResponse; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Routing\Redirector; @@ -62,24 +63,38 @@ function ($request, $next) { } /** - * Move a rule group down. + * Move a rule group in either direction. * - * @param RuleGroup $ruleGroup + * @param Request $request * - * @return RedirectResponse|Redirector + * @return JsonResponse */ - public function down(RuleGroup $ruleGroup) + public function moveGroup(Request $request): JsonResponse { - $maxOrder = $this->repository->maxOrder(); - $order = (int)$ruleGroup->order; - if ($order < $maxOrder) { - $newOrder = $order + 1; - $this->repository->setOrder($ruleGroup, $newOrder); + $groupId = (int)$request->get('id'); + $ruleGroup= $this->repository->find($groupId); + if(null !== $ruleGroup) { + $direction = $request->get('direction'); + if('down' === $direction) { + $maxOrder = $this->repository->maxOrder(); + $order = (int)$ruleGroup->order; + if ($order < $maxOrder) { + $newOrder = $order + 1; + $this->repository->setOrder($ruleGroup, $newOrder); + } + } + if('up' === $direction) { + $order = (int)$ruleGroup->order; + if ($order > 1) { + $newOrder = $order - 1; + $this->repository->setOrder($ruleGroup, $newOrder); + } + } } - - return redirect(route('rules.index')); + return new JsonResponse(['OK']); } + /** * Edit a rule group. * @@ -106,25 +121,6 @@ public function edit(Request $request, RuleGroup $ruleGroup) return prefixView('rules.rule-group.edit', compact('ruleGroup', 'subTitle')); } - /** - * Move the rule group up. - * - * @param RuleGroup $ruleGroup - * - * @return RedirectResponse|Redirector - * - */ - public function up(RuleGroup $ruleGroup) - { - $order = (int)$ruleGroup->order; - if ($order > 1) { - $newOrder = $order - 1; - $this->repository->setOrder($ruleGroup, $newOrder); - } - - return redirect(route('rules.index')); - } - /** * Update the rule group. *
app/Repositories/RuleGroup/RuleGroupRepository.php+0 −3 modified@@ -329,10 +329,8 @@ public function maxOrder(): int */ public function resetOrder(): bool { - $this->user->ruleGroups()->where('active', false)->update(['order' => 0]); $set = $this->user ->ruleGroups() - ->where('active', true) ->whereNull('deleted_at') ->orderBy('order', 'ASC') ->orderBy('title', 'DESC') @@ -363,7 +361,6 @@ public function resetRuleOrder(RuleGroup $ruleGroup): bool { $set = $ruleGroup->rules() ->orderBy('order', 'ASC') - ->where('active', true) ->orderBy('title', 'DESC') ->orderBy('updated_at', 'DESC') ->get(['rules.*']);
public/v1/js/ff/rules/index.js+29 −0 modified@@ -59,6 +59,32 @@ function readCookie(name) { return null; } +function moveRuleGroup(e) { + let box = $(e.currentTarget); + var direction = box.data('direction'); + var groupId = box.data('id'); + + $.post(moveRuleGroupUrl, {_token: token, direction: direction, id: groupId}).then(function () { + location.reload(); + }).fail(function() { + alert('I failed :('); + }); + + return false; +} + +function duplicateRule(e) { + let box = $(e.currentTarget); + var ruleId = box.data('id'); + $.post(duplicateRuleUrl, {_token: token, id: ruleId}).then(function () { + location.reload(); + }).fail(function() { + alert('I failed :('); + }); + + return false; +} + $(function () { "use strict"; @@ -71,6 +97,9 @@ $(function () { } ); + $('.move-group').click(moveRuleGroup); + $('.duplicate-rule').click(duplicateRule); + $('.rules-box').each(function (i, v) { var box = $(v); var groupId = box.data('group');
resources/views/v1/rules/index.twig+7 −3 modified@@ -45,11 +45,11 @@ class="fa fa-fw fa-power-off"></span> {{ trans('firefly.apply_rule_group_selection', {title: ruleGroup.title}) }} </a></li> {% if ruleGroup.order > 1 %} - <li><a href="{{ route('rule-groups.up',ruleGroup.id) }}"><span + <li><a href="#" class="move-group" data-direction="up" data-id="{{ ruleGroup.id }}"><span class="fa fa-fw fa-arrow-up"></span> {{ 'move_rule_group_up'|_ }}</a></li> {% endif %} {% if ruleGroup.order < ruleGroups|length %} - <li><a href="{{ route('rule-groups.down',ruleGroup.id) }}"><span + <li><a href="#" class="move-group" data-direction="down" data-id="{{ ruleGroup.id }}"><span class="fa fa-fw fa-arrow-down"></span> {{ 'move_rule_group_down'|_ }} </a></li> {% endif %} @@ -105,7 +105,7 @@ {% endif %} {# duplicate rule #} - <a href="{{ route('rules.duplicate',rule.id) }}" class="btn btn-default" title=" {{ trans('firefly.duplicate_rule', {title: rule.title}) }}"><span class="fa fa-fw fa-copy"></span></a> + <a href="#" class="btn btn-default duplicate-rule" data-id="{{ rule.id }}" title=" {{ trans('firefly.duplicate_rule', {title: rule.title}) }}"><span class="fa fa-fw fa-copy"></span></a> </div> </td> <td class="markdown"> @@ -195,6 +195,10 @@ {% endblock %} {% block scripts %} + <script type="text/javascript" nonce="{{ JS_NONCE }}"> + var moveRuleGroupUrl = '{{ route('rule-groups.move') }}'; + var duplicateRuleUrl = '{{ route('rules.duplicate') }}'; + </script> <script type="text/javascript" src="v1/js/lib/jquery-ui.min.js?v={{ FF_VERSION }}" nonce="{{ JS_NONCE }}"></script> <script type="text/javascript" src="v1/js/ff/rules/index.js?v={{ FF_VERSION }}" nonce="{{ JS_NONCE }}"></script> {% endblock %}
routes/web.php+5 −4 modified@@ -917,7 +917,7 @@ static function () { Route::get('create-from-bill/{bill}', ['uses' => 'Rule\CreateController@createFromBill', 'as' => 'create-from-bill']); Route::get('create-from-journal/{tj}', ['uses' => 'Rule\CreateController@createFromJournal', 'as' => 'create-from-journal']); Route::post('store', ['uses' => 'Rule\CreateController@store', 'as' => 'store']); - Route::get('duplicate/{rule}', ['uses' => 'Rule\CreateController@duplicate', 'as' => 'duplicate']); + Route::post('duplicate', ['uses' => 'Rule\CreateController@duplicate', 'as' => 'duplicate']); // delete controller Route::get('delete/{rule}', ['uses' => 'Rule\DeleteController@delete', 'as' => 'delete']); @@ -949,10 +949,11 @@ static function () { Route::get('create', ['uses' => 'RuleGroup\CreateController@create', 'as' => 'create']); Route::get('edit/{ruleGroup}', ['uses' => 'RuleGroup\EditController@edit', 'as' => 'edit']); Route::get('delete/{ruleGroup}', ['uses' => 'RuleGroup\DeleteController@delete', 'as' => 'delete']); - Route::get('up/{ruleGroup}', ['uses' => 'RuleGroup\EditController@up', 'as' => 'up']); - Route::get('down/{ruleGroup}', ['uses' => 'RuleGroup\EditController@down', 'as' => 'down']); - Route::get('select/{ruleGroup}', ['uses' => 'RuleGroup\ExecutionController@selectTransactions', 'as' => 'select-transactions']); + // new route to move rule groups: + Route::post('move', ['uses' => 'RuleGroup\EditController@moveGroup', 'as' => 'move']); + + Route::get('select/{ruleGroup}', ['uses' => 'RuleGroup\ExecutionController@selectTransactions', 'as' => 'select-transactions']); Route::post('store', ['uses' => 'RuleGroup\CreateController@store', 'as' => 'store']); Route::post('update/{ruleGroup}', ['uses' => 'RuleGroup\EditController@update', 'as' => 'update']); Route::post('destroy/{ruleGroup}', ['uses' => 'RuleGroup\DeleteController@destroy', 'as' => 'destroy']);
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-pfj7-w373-gqchghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2021-3900ghsaADVISORY
- github.com/firefly-iii/firefly-iii/commit/c2c8c42ef3194d1aeba8c48240fe2e9063f77635ghsax_refsource_MISCWEB
- huntr.dev/bounties/909e55b6-ef02-4143-92e4-bc3e8397db76ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.