VYPR
Moderate severityNVD Advisory· Published Oct 20, 2023· Updated Sep 11, 2024

Cross-Site Request Forgery (CSRF) in modoboa/modoboa

CVE-2023-5690

Description

Cross-Site Request Forgery (CSRF) in GitHub repository modoboa/modoboa prior to 2.2.2.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

A CSRF vulnerability in Modoboa prior to 2.2.2 allows attackers to perform a logout action without proper request validation.

Overview

CVE-2023-5690 is a Cross-Site Request Forgery (CSRF) vulnerability in the open-source mail hosting platform Modoboa, affecting versions prior to 2.2.2 [1]. The flaw exists in the logout functionality, where the dologout view was not restricted to POST requests, and the user_menu template tag passed the user object directly without requiring a POST method for the logout link [1]. This allowed an attacker to trigger a logout on behalf of an authenticated user by crafting a malicious link or form that the victim would visit.

Exploitation

An attacker can exploit this vulnerability by convincing an authenticated Modoboa user to click on a crafted URL or submit a hidden form. Since the logout endpoint did not enforce the POST method, a simple GET request or any cross-origin request could initiate the logout [1]. The fix introduced the @require_http_methods(["POST"]) decorator on the dologout view and changed the logout menu entry method to POST, preventing CSRF attacks [1].

Impact

Successful exploitation allows an attacker to forcefully log out a victim from their Modoboa session [1]. This can be used to interrupt the user's work, contribute to a denial-of-service condition, or as part of a social engineering chain that makes the user believe they must re-authenticate on a phishing site. The vulnerability does not directly expose data, but it can facilitate further attacks by disrupting the user's session.

Mitigation

The vulnerability was addressed in Modoboa version 2.2.2 by commit 23e4c255 [1]. Users should upgrade to this version or later to remediate the issue. The official advisory is available from huntr.dev and the PyPI advisory database [2][3]. Modoboa is an actively maintained project, and no workarounds have been provided for older versions [4].

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
modoboaPyPI
< 2.2.22.2.2

Affected products

2
  • ghsa-coords
    Range: < 2.2.2
  • modoboa/modoboa/modoboav5
    Range: unspecified

Patches

1
23e4c25511c6

Merge pull request #3090 from modoboa/fix/csrf_issue_logout

https://github.com/modoboa/modoboaAntoine NguyenOct 17, 2023via ghsa
6 files changed · +47 20
  • modoboa/core/templatetags/core_tags.py+12 8 modified
    @@ -79,11 +79,11 @@ def admin_menu(selection, user):
     
     
     @register.simple_tag
    -def user_menu(user, selection):
    +def user_menu(request, selection):
         entries = [
             {"name": "user",
              "img": "fa fa-user",
    -         "label": user.fullname,
    +         "label": request.user.fullname,
              "menu": [
                     {"name": "settings",
                      "img": "fa fa-list",
    @@ -93,17 +93,21 @@ def user_menu(user, selection):
         ]
     
         extra_entries = signals.extra_user_menu_entries.send(
    -        sender="user_menu", location="options_menu", user=user)
    +        sender="user_menu", location="options_menu", user=request.user)
         extra_entries = reduce(
             lambda a, b: a + b, [entry[1] for entry in extra_entries])
         entries[0]["menu"] += (
    -        extra_entries + [{"name": "logout",
    -                          "url": reverse("core:logout"),
    -                          "label": _("Logout"),
    -                          "img": "fa fa-sign-out"}]
    +        extra_entries + [{
    +            "name": "logout",
    +            "url": reverse("core:logout"),
    +            "label": _("Logout"),
    +            "img": "fa fa-sign-out",
    +            "method": "post"
    +        }]
         )
         return render_to_string("common/menulist.html", {
    -        "selection": selection, "entries": entries, "user": user
    +        "request": request, "selection": selection,
    +        "entries": entries, "user": request.user
         })
     
     
    
  • modoboa/core/views/auth.py+2 0 modified
    @@ -16,6 +16,7 @@
     from django.utils.translation import gettext as _
     from django.views import generic
     from django.views.decorators.cache import never_cache
    +from django.views.decorators.http import require_http_methods
     
     from django.contrib.auth import (
         authenticate, login, logout, views as auth_views
    @@ -126,6 +127,7 @@ def dologin(request):
     dologin = never_cache(dologin)
     
     
    +@require_http_methods(["POST"])
     def dologout(request):
         """Logout current user."""
         if not request.user.is_anonymous:
    
  • modoboa/lib/templatetags/lib_tags.py+10 1 modified
    @@ -3,7 +3,7 @@
     from datetime import datetime
     
     from django import template
    -from django.template import Context, Template
    +from django.template import Context, RequestContext, Template
     from django.utils.safestring import mark_safe
     from django.utils.translation import gettext as _
     
    @@ -51,6 +51,15 @@ def render_link(linkdef, mdclass=""):
         return t.render(Context({"link": linkdef, "mdclass": mdclass}))
     
     
    +@register.simple_tag
    +def render_post_link(linkdef, request):
    +    t = Template("""<form method="post" action="{{ link.url }}">
    +{% csrf_token %}
    +<a class="menu-link" href="#" onclick="this.parentNode.submit()">{% if link.img %}<i class="{{ link.img }}"></i>{% endif %}{{ link.label }}</a>
    +</form>""")
    +    return t.render(RequestContext(request, {"link": linkdef}))
    +
    +
     @register.simple_tag
     def progress_color(value):
         value = int(value)
    
  • modoboa/static/css/custom.css+15 7 modified
    @@ -1,6 +1,6 @@
     /* ------- Media ------- */
     
    -.sidebar { 
    +.sidebar {
         display: none;
     }
     
    @@ -67,7 +67,7 @@
     
      /* --------- general --------- */
     
    -.container-fluid { 
    +.container-fluid {
         padding-left: 0;
         padding-right: 0;
     }
    @@ -101,7 +101,7 @@
     
     
     /* Global */
    -body { 
    +body {
         padding-top: 50px;
         padding-bottom: 50px;
     }
    @@ -168,7 +168,15 @@ label {
         table-layout: fixed;
     }
     
    -
    +.menu-link {
    +	display: block;
    +	padding: 3px 20px;
    +	clear: both;
    +	font-weight: 400;
    +	line-height: 1.42857143;
    +	color: #333;
    +	white-space: nowrap;
    +}
     
     /* --------- main page --------- */
     
    @@ -202,7 +210,7 @@ html body div.navbar div.container-fluid div#navigate-navbar-collapse.collapse u
         padding-left: 20px;*/
     }
     
    -.nav-sidebar > li > a:hover { 
    +.nav-sidebar > li > a:hover {
         background-color: #eee;
     }
     
    @@ -230,7 +238,7 @@ html body div.navbar div.container-fluid div#navigate-navbar-collapse.collapse u
          margin-bottom: 4px;
      }
     
    -.main { 
    +.main {
         padding: 20px 15px;
     }
     
    @@ -280,7 +288,7 @@ table td[name="actions"] {
     
      /* modals */
     
    -.modal-body .nav-pills { 
    +.modal-body .nav-pills {
         margin-bottom: 15px;
     }
     
    
  • modoboa/templates/common/menulist.html+7 3 modified
    @@ -4,19 +4,23 @@
       <li class="divider">&nbsp;</li>
       {% else %}
         {% if entry.menu %}
    -  <li class="dropdown {{ entry.class }}{% if selection == entry.name %} active{% endif %}"> 
    +  <li class="dropdown {{ entry.class }}{% if selection == entry.name %} active{% endif %}">
         <a class="dropdown-toggle" name="{{ entry.name }}" data-toggle="dropdown" href="{{ entry.url }}">
           {% if entry.img %}<span class="{{ entry.img }}"></span> {% endif %}{{ entry.label }}
         </a>
         <ul class="dropdown-menu" {% if entry.width %}style="width: {{ entry.width }}px"{% endif %}>
           {% for sentry in entry.menu %}
    -      <li>{% render_link sentry %}</li>
    +      <li>{% if sentry.method == "post" %}{% render_post_link sentry request %}{% else %}{% render_link sentry %}{% endif %}</li>
           {% endfor %}
         </ul>
       </li>
         {% else %}
       <li class="{% if selection == entry.name %}active{% endif %}">
    -    {% render_link entry %}
    +    {% if entry.method == "post" %}
    +      {% render_post_link entry request %}
    +    {% else %}
    +      {% render_link entry %}
    +    {% endif %}
       </li>
         {% endif %}
       {% endif %}
    
  • modoboa/templates/nlayout.html+1 1 modified
    @@ -60,7 +60,7 @@
                 {% endif %}
               </ul>
               <ul class="nav navbar-nav navbar-right">
    -            {% if selection %}{% user_menu user selection %}{% else %}{% user_menu user "" %}{% endif %}
    +            {% if selection %}{% user_menu request selection %}{% else %}{% user_menu request "" %}{% endif %}
               </ul>
               {% include "common/top_notifications.html" %}
             </div>
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

5

News mentions

0

No linked articles in our index yet.