CVE-2021-3819 (published 2021-09-27T13:15Z)

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

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

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-09-20 06:39:10+02:00
msg: Convert GET routes to POST.
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: CurrencyController.php
change_type: ModificationType.MODIFY
diff: @@ -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'));
     }

programming_language: PHP
filename: CreateController.php
change_type: ModificationType.MODIFY
diff: @@ -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,9 +52,10 @@ 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);
             }
@@ -59,28 +63,35 @@ static function ($request, $next) {
     }
 
     /**
-     * @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])]);
     }
 
     /**

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

programming_language: JavaScript
filename: groups.js
change_type: ModificationType.MODIFY
diff: @@ -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;
+}

programming_language: JavaScript
filename: show.js
change_type: ModificationType.MODIFY
diff: @@ -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

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

programming_language: HTML
filename: groups.twig
change_type: ModificationType.MODIFY
diff: @@ -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>

programming_language: HTML
filename: show.twig
change_type: ModificationType.MODIFY
diff: @@ -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>

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

programming_language: PHP