CVE-2021-3900 (published 2021-10-27T18:15Z)

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

Severity: MEDIUM
CVSS v2 base score: 4.3 (AV:N/AC:M/Au:N/C:N/I:P/A:N)
CVSS v3 base score: 6.5, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N)

Weakness information:
cwe_id: CWE-352
cwe_name: Cross-Site Request Forgery (CSRF)
description: The web application does not, or can not, sufficiently verify whether a well-formed, valid, consistent request was intentionally provided by the user who submitted the request.
extended_description: When a web server is designed to receive a request from a client without any mechanism for verifying that it was intentionally sent, then it might be possible for an attacker to trick a client into making an unintentional request to the web server which will be treated as an authentic request. This can be done via a URL, image load, XMLHttpRequest, etc. and can result in exposure of data or unintended code execution.
         

Fix information:
repo_url: https://github.com/firefly-iii/firefly-iii
commit_details: repo_url: https://github.com/firefly-iii/firefly-iii
author: James Cole
author_date: 2021-10-23 09:29:07+02:00
msg: Catch CSRF issues
repository: repo_url: https://github.com/firefly-iii/firefly-iii
repo_name: firefly-iii/firefly-iii
description: Firefly III: a personal finances manager
repo_language: PHP
owner: firefly-iii
file_changes: filename: CreateController.php
change_type: ModificationType.MODIFY
diff: @@ -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']);
     }
 
     /**

programming_language: PHP
filename: EditController.php
change_type: ModificationType.MODIFY
diff: @@ -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.
      *

programming_language: PHP
filename: RuleGroupRepository.php
change_type: ModificationType.MODIFY
diff: @@ -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.*']);

programming_language: PHP
filename: index.js
change_type: ModificationType.MODIFY
diff: @@ -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');

programming_language: JavaScript
filename: index.twig
change_type: ModificationType.MODIFY
diff: @@ -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 %}

programming_language: HTML
filename: web.php
change_type: ModificationType.MODIFY
diff: @@ -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']);

programming_language: PHP