CVE-2022-4646 (published 2022-12-22T02:15Z)

Cross-Site Request Forgery (CSRF) in GitHub repository ikus060/rdiffweb prior to 2.5.4.

CVSS v3 base score: 6.5, MEDIUM (CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:N/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/ikus060/rdiffweb
commit_details: repo_url: https://github.com/ikus060/rdiffweb
author: Patrik Dufresne
author_date: 2022-12-19 08:18:41-05:00
msg: Add CSRF verification on `/logout`
repository: repo_url: https://github.com/ikus060/rdiffweb
repo_name: ikus060/rdiffweb
description: A simplified backup management software for quick access to your archives through an efficient web interface.
repo_language: Python
owner: ikus060
file_changes: filename: README.md
change_type: ModificationType.MODIFY
diff: @@ -111,7 +111,8 @@ Professional support for Rdiffweb is available by contacting [IKUS Soft](https:/
 ## Next Release - 2.5.4
 
 * Discard `X-Forwarded-Host` headers credit to [Anishka Shukla](https://github.com/anishkashukla)
-* Create proper symbolic link of chartkick.js on Ubuntu Jammy to fix loading of Charts in web interface
+* Create proper symbolic link of `chartkick.js` on Ubuntu Jammy to fix loading of Charts in web interface
+* Add CSRF verification on `/logout` credits to [reza.duty](https://rezaduty.me)
 
 ## 2.5.3 (2022-12-05)
 

programming_language: Markdown
filename: test_controller.py
change_type: ModificationType.MODIFY
diff: @@ -77,7 +77,7 @@ class ControllerTest(rdiffweb.test.WebCase):
         """
         Check if the theme is properly configure.
         """
-        self.getPage('/logout')
+        self.getPage('/logout', method="POST")
         self.getPage(path)
         self.assertStatus(200)
         # Test with invalid method.

programming_language: Python
filename: test_page_admin_users.py
change_type: ModificationType.MODIFY
diff: @@ -325,7 +325,7 @@ class AbstractAdminTest(rdiffweb.test.WebCase):
         """
         # Create another admin user
         self._add_user('admin2', '', 'pr3j5Dwi', '', UserObject.ADMIN_ROLE)
-        self.getPage("/logout")
+        self.getPage("/logout", method="POST")
         self.assertStatus(303)
         self.assertHeaderItemValue('Location', self.baseurl + '/')
         self._login('admin2', 'pr3j5Dwi')

programming_language: Python
filename: test_page_login.py
change_type: ModificationType.MODIFY
diff: @@ -183,7 +183,7 @@ class LoginPageTest(rdiffweb.test.WebCase):
 
     def test_login_persistent(self):
         # Given a user authenticated with persistent
-        self.getPage('/logout/')
+        self.getPage('/logout', method="POST")
         self.assertStatus(303)
         self.getPage(
             '/login/', method='POST', body={'login': self.USERNAME, 'password': self.PASSWORD, 'persistent': '1'}
@@ -312,7 +312,7 @@ class LogoutPageTest(rdiffweb.test.WebCase):
     def test_getpage_without_login(self):
         # Given an unauthenticated user
         # When Accessing logout page directly
-        self.getPage('/logout')
+        self.getPage('/logout', method="POST")
         # Then user is redirect to root '/'
         self.assertStatus('303 See Other')
         self.assertHeaderItemValue('Location', self.baseurl + '/')
@@ -332,7 +332,7 @@ class LogoutPageTest(rdiffweb.test.WebCase):
         self.getPage("/prefs/general")
         self.assertStatus('200 OK')
         # When logout
-        self.getPage('/logout')
+        self.getPage('/logout', method="POST")
         # Then a new session id is generated
         self.assertNotEqual(prev_session_id, self.session_id)
         # Then user is redirected to root page

programming_language: Python
filename: test_page_mfa.py
change_type: ModificationType.MODIFY
diff: @@ -52,7 +52,7 @@ class MfaPageTest(rdiffweb.test.WebCase):
 
     def test_get_without_login(self):
         # Given an unauthenticated user
-        self.getPage("/logout")
+        self.getPage("/logout", method="POST")
         self.assertStatus(303)
         # When requesting /mfa/
         self.getPage("/mfa/")

programming_language: Python
filename: test_secure_headers.py
change_type: ModificationType.MODIFY
diff: @@ -58,7 +58,7 @@ class SecureHeadersTest(rdiffweb.test.WebCase):
             ('/invalid', 404),
             ('/browse/invalid', 404),
             ('/login', 301),
-            ('/logout', 303),
+            ('/logout', 405),
         ]
     )
     def test_cookie_with_https_http_error(self, url, expected_error_code):

programming_language: Python
filename: layout.html
change_type: ModificationType.MODIFY
diff: @@ -85,13 +85,16 @@
                   <i aria-hidden="true" class="fa fa-user"></i>
                   {{ username }}
                 </a>
-                <div aria-labelledby="navbarDropdown" class="dropdown-menu">
-                  <h6 class="dropdown-header">{% trans %}Signed in as{% endtrans %} {{ username }}</h6>
-                  <a class="dropdown-item" href="{{ url_for('prefs', 'general') }}">{% trans %}Edit profile{% endtrans %}</a>
-                  <a class="dropdown-item" href="{{ url_for('prefs', 'notification') }}">{% trans %}Notifications{% endtrans %}</a>
-                  <div class="dropdown-divider"></div>
-                  <a class="dropdown-item" href="{{ url_for('logout') }}">Logout</a>
-                </div>
+                <form action="{{ url_for('logout') }}" method="post">
+                  <div aria-labelledby="navbarDropdown" class="dropdown-menu">
+                    <h6 class="dropdown-header">{% trans %}Signed in as{% endtrans %} {{ username }}</h6>
+                    <a class="dropdown-item" href="{{ url_for('prefs', 'general') }}">{% trans %}Edit profile{% endtrans %}</a>
+                    <a class="dropdown-item" href="{{ url_for('prefs', 'notification') }}">{% trans %}Notifications{% endtrans %}</a>
+                    <div class="dropdown-divider"></div>
+                    {# Logout button #}
+                    <button type="submit" class="dropdown-item">{% trans %}Logout{% endtrans %}</button>
+                  </div>
+                </form>
               </li>
             </ul>
           </div>

programming_language: HTML
filename: mfa.html
change_type: ModificationType.MODIFY
diff: @@ -11,10 +11,12 @@
           </p>
           {{ form }}
         </form>
-        <a class="btn-link btn-sm btn-block text-center"
-           href="{{ url_for('logout')}}">
-          {% trans %}Login with a different account{% endtrans %}
-        </a>
+        {# Logout button #}
+        <form action="{{ url_for('logout') }}" method="post">
+          <button type="submit" class="btn btn-link btn-sm btn-block text-center">
+            {% trans %}Login with a different account{% endtrans %}
+          </button>
+        </form>
       </div>
     </div>
   </div>

programming_language: HTML
filename: test.py
change_type: ModificationType.MODIFY
diff: @@ -176,7 +176,7 @@ class WebCase(helper.CPWebCase):
         return json.loads(self.body.decode('utf8'))
 
     def _login(self, username=USERNAME, password=PASSWORD):
-        self.getPage("/logout")
+        self.getPage("/logout", method="POST")
         self.getPage("/login/", method='POST', body={'login': username, 'password': password})
         self.assertStatus('303 See Other')
 

programming_language: Python
filename: auth_form.py
change_type: ModificationType.MODIFY
diff: @@ -104,6 +104,8 @@ class CheckAuthForm(cherrypy.Tool):
 
         # Clear session when browsing /logout
         if request.path_info == logout_url or request.path_info.startswith(logout_url):
+            if request.method != 'POST':
+                raise cherrypy.HTTPError(405)
             self.logout()
             raise cherrypy.HTTPRedirect('/')
 

programming_language: Python