CVE-2021-3932: Cross-Site Request Forgery (CSRF) (CWE-352)
value: twill is vulnerable to Cross-Site Request Forgery (CSRF)
CWE 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.
CVSS v3 base score: 4.3
Published: 2021-11-13T09:15Z
Repository: https://github.com/area17/twill
Language: PHP
Files changed: frontend/js/behaviors/logoutButton.js, frontend/js/main.js, routes/auth.php, views/layouts/main.blade.php, views/partials/navigation/_overlay_navigation.blade.php, views/partials/navigation/_user.blade.php
Commit date: 2021-11-07 03:07:12 +0100
Commit message: Use POST action for logout links

Diff: diff --git a/frontend/js/behaviors/logoutButton.js b/frontend/js/behaviors/logoutButton.js
new file mode 100644
index 00000000..f9db5207
--- /dev/null
+++ b/frontend/js/behaviors/logoutButton.js
@@ -0,0 +1,16 @@
+// POST logout action
+
+const logoutButton = function () {
+  const logoutForm = document.querySelector('[data-logout-form]')
+
+  if (!logoutForm) return
+
+  document.body.addEventListener('click', e => {
+    if (e.target.hasAttribute('data-logout-btn')) {
+      e.preventDefault()
+      logoutForm.submit()
+    }
+  })
+}
+
+export default logoutButton
diff --git a/frontend/js/main.js b/frontend/js/main.js
index 7a3ba3e3..a13902ca 100644
--- a/frontend/js/main.js
+++ b/frontend/js/main.js
@@ -2,16 +2,18 @@
 import 'styles/app.scss'
 // General behaviors
 import Vue from 'vue'
 import navToggle from '@/behaviors/navToggle'
 import showEnvLine from '@/behaviors/showEnvLine'
+import logoutButton from '@/behaviors/logoutButton'
 import search from '@/main-search'
 import merge from 'lodash/merge'
 
 const A17Init = function () {
   navToggle()
   showEnvLine()
+  logoutButton()
 }
 
 // User header dropdown
 /* eslint-disable no-new */
 /* eslint no-unused-vars: "off" */
diff --git a/routes/auth.php b/routes/auth.php
index 9912b19f..b08abb25 100644
--- a/routes/auth.php
+++ b/routes/auth.php
@@ -3,11 +3,11 @@
 use Illuminate\Support\Facades\Route;
 
 if (config('twill.enabled.users-management')) {
     Route::get('login', 'LoginController@showLoginForm')->name('login.form');
     Route::post('login', 'LoginController@login')->name('login');
-    Route::get('logout', 'LoginController@logout')->name('logout');
+    Route::post('logout', 'LoginController@logout')->name('logout');
 
     Route::get('password/reset', 'ForgotPasswordController@showLinkRequestForm')->name('password.reset.link');
     Route::post('password/email', 'ForgotPasswordController@sendResetLinkEmail')->name('password.reset.email');
     Route::get('password/reset/{token}', 'ResetPasswordController@showResetForm')->name('password.reset.form');
     Route::get('password/welcome/{token}', 'ResetPasswordController@showWelcomeForm')->name('password.reset.welcome.form');
diff --git a/views/layouts/main.blade.php b/views/layouts/main.blade.php
index 75069efc..27876f16 100644
--- a/views/layouts/main.blade.php
+++ b/views/layouts/main.blade.php
@@ -69,10 +69,15 @@
                     </span>
                 </div>
                 @include('twill::partials.footer')
             </section>
         </div>
+
+        <form class="visually-hidden" method="POST" action="{{ route('admin.logout') }}" data-logout-form>
+            @csrf
+        </form>
+
         <script>
             window['{{ config('twill.js_namespace') }}'] = {};
             window['{{ config('twill.js_namespace') }}'].version = '{{ config('twill.version') }}';
             window['{{ config('twill.js_namespace') }}'].twillLocalization = {!! json_encode($twillLocalization) !!};
             window['{{ config('twill.js_namespace') }}'].STORE = {};
diff --git a/views/partials/navigation/_overlay_navigation.blade.php b/views/partials/navigation/_overlay_navigation.blade.php
index 11ce4770..06aa2e0e 100644
--- a/views/partials/navigation/_overlay_navigation.blade.php
+++ b/views/partials/navigation/_overlay_navigation.blade.php
@@ -20,11 +20,11 @@
                         <a href="#" data-closenav-btn data-medialib-btn>{{ twillTrans('twill::lang.nav.media-library') }}</a><br />
                     @endif
                     @if(isset($currentUser))
                         <a href="{{ route('admin.users.index') }}">{{ twillTrans('twill::lang.nav.cms-users') }}</a><br />
                         <a href="{{ route('admin.users.edit', $currentUser->id) }}">{{ twillTrans('twill::lang.nav.settings') }}</a><br />
-                        <a href="{{ route('admin.logout') }}">{{ twillTrans('twill::lang.nav.logout') }}</a>
+                        <a href="#" data-logout-btn>{{ twillTrans('twill::lang.nav.logout') }}</a>
                     @endif
                 </div>
             </div>
         </nav>
     </header>
diff --git a/views/partials/navigation/_user.blade.php b/views/partials/navigation/_user.blade.php
index c41573f6..e17a2ff5 100644
--- a/views/partials/navigation/_user.blade.php
+++ b/views/partials/navigation/_user.blade.php
@@ -12,9 +12,9 @@
         <div slot="dropdown__content">
             @can('manage-users')
                 <a href="{{ route('admin.users.index') }}">{{ twillTrans('twill::lang.nav.cms-users') }}</a>
             @endcan
             <a href="{{ route('admin.users.edit', $currentUser->id) }}">{{ twillTrans('twill::lang.nav.settings') }}</a>
-            <a href="{{ route('admin.logout') }}">{{ twillTrans('twill::lang.nav.logout') }}</a>
+            <a href="#" data-logout-btn>{{ twillTrans('twill::lang.nav.logout') }}</a>
         </div>
     </a17-dropdown>
 @endif

Vulnerable code:     Route::get('logout', 'LoginController@logout')->name('logout');
                        <a href="{{ route('admin.logout') }}">{{ twillTrans('twill::lang.nav.logout') }}</a>
            <a href="{{ route('admin.logout') }}">{{ twillTrans('twill::lang.nav.logout') }}</a>
Fixed code: // POST logout action

const logoutButton = function () {
  const logoutForm = document.querySelector('[data-logout-form]')

  if (!logoutForm) return

  document.body.addEventListener('click', e => {
    if (e.target.hasAttribute('data-logout-btn')) {
      e.preventDefault()
      logoutForm.submit()
    }
  })
}

export default logoutButton
import logoutButton from '@/behaviors/logoutButton'
  logoutButton()
    Route::post('logout', 'LoginController@logout')->name('logout');

        <form class="visually-hidden" method="POST" action="{{ route('admin.logout') }}" data-logout-form>
            @csrf
        </form>

                        <a href="#" data-logout-btn>{{ twillTrans('twill::lang.nav.logout') }}</a>
            <a href="#" data-logout-btn>{{ twillTrans('twill::lang.nav.logout') }}</a>