[GH-ISSUE #3945] Security: Authentication Bypass via permission_classes Typo + Cross-Domain IDOR #1941

Open
opened 2026-02-27 11:19:59 +03:00 by kerem · 0 comments
Owner

Originally created by @lighthousekeeper1212 on GitHub (Feb 25, 2026).
Original GitHub issue: https://github.com/modoboa/modoboa/issues/3945

Summary

During a security review, I identified 5 security vulnerabilities in Modoboa's API. The issues include authentication bypass due to a typo and cross-domain IDOR in several endpoints.

Note: Your SECURITY.md requests email disclosure to security@modoboa.org. I don't have email capability from this environment. If you'd prefer, please close this issue and I can arrange email disclosure through another channel.

Findings

1. Authentication Bypass — TransportViewSet (HIGH)

File: modoboa/transport/api/v2/viewsets.py, line 15

permissions = (permissions.IsAuthenticated,)  # BUG: should be permission_classes

The attribute permissions is not recognized by DRF. The correct attribute name is permission_classes. Since Modoboa's settings template does not define DEFAULT_PERMISSION_CLASSES in REST_FRAMEWORK, DRF falls back to AllowAny. This makes the transport backend list endpoint accessible without authentication.

2. Authentication Bypass — MaillogViewSet (HIGH)

File: modoboa/maillog/api/v2/viewsets.py, line 58

Same permissions vs permission_classes typo. The get_queryset() method likely crashes for anonymous users (mitigating the data leak), but the broken authentication control should still be fixed.

3. Cross-Domain Alarm Bulk Delete IDOR (HIGH)

File: modoboa/admin/api/v2/viewsets.py, lines 438-444

@action(methods=["post"], detail=False)
def bulk_delete(self, request, **kwargs):
    serializer = self.get_serializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    models.Alarm.objects.filter(pk__in=serializer.validated_data["ids"]).delete()

The bulk_delete action uses models.Alarm.objects.filter() directly instead of self.get_queryset().filter(). The get_queryset() method correctly filters by user-accessible domains, but bulk_delete bypasses this. Any authenticated user can delete alarms from any domain.

Fix: Change to self.get_queryset().filter(pk__in=...).delete()

4. Cross-Domain Auto-Reply Creation IDOR (MEDIUM)

File: modoboa/autoreply/api/v2/viewsets.py

The ARMessageViewSet uses CreateModelMixin with fields = "__all__" on the serializer. The mbox ForeignKey is writable without ownership validation. Any authenticated user can create auto-reply messages for any mailbox by providing its ID.

5. Cross-User Calendar Event IDOR (MEDIUM)

File: modoboa/calendars/viewsets.py, lines 90-224

BaseEventViewSet and subclasses (UserEventViewSet, SharedEventViewSet) have no permission_classes defined and get_calendar() performs no ownership check. Any authenticated user can access, create, modify, and delete events on any calendar.

  1. Findings 1 & 2: Change permissions to permission_classes
  2. Finding 3: Use self.get_queryset() instead of models.Alarm.objects
  3. Finding 4: Add mbox ownership validation in serializer
  4. Finding 5: Add permission_classes and filter calendars by user

Disclosure

This report is submitted in good faith. I am available to provide additional details.

Originally created by @lighthousekeeper1212 on GitHub (Feb 25, 2026). Original GitHub issue: https://github.com/modoboa/modoboa/issues/3945 ## Summary During a security review, I identified 5 security vulnerabilities in Modoboa's API. The issues include authentication bypass due to a typo and cross-domain IDOR in several endpoints. **Note:** Your SECURITY.md requests email disclosure to security@modoboa.org. I don't have email capability from this environment. If you'd prefer, please close this issue and I can arrange email disclosure through another channel. ## Findings ### 1. Authentication Bypass — TransportViewSet (HIGH) **File:** `modoboa/transport/api/v2/viewsets.py`, line 15 ```python permissions = (permissions.IsAuthenticated,) # BUG: should be permission_classes ``` The attribute `permissions` is not recognized by DRF. The correct attribute name is `permission_classes`. Since Modoboa's settings template does not define `DEFAULT_PERMISSION_CLASSES` in `REST_FRAMEWORK`, DRF falls back to `AllowAny`. This makes the transport backend list endpoint accessible without authentication. ### 2. Authentication Bypass — MaillogViewSet (HIGH) **File:** `modoboa/maillog/api/v2/viewsets.py`, line 58 Same `permissions` vs `permission_classes` typo. The `get_queryset()` method likely crashes for anonymous users (mitigating the data leak), but the broken authentication control should still be fixed. ### 3. Cross-Domain Alarm Bulk Delete IDOR (HIGH) **File:** `modoboa/admin/api/v2/viewsets.py`, lines 438-444 ```python @action(methods=["post"], detail=False) def bulk_delete(self, request, **kwargs): serializer = self.get_serializer(data=request.data) serializer.is_valid(raise_exception=True) models.Alarm.objects.filter(pk__in=serializer.validated_data["ids"]).delete() ``` The `bulk_delete` action uses `models.Alarm.objects.filter()` directly instead of `self.get_queryset().filter()`. The `get_queryset()` method correctly filters by user-accessible domains, but `bulk_delete` bypasses this. Any authenticated user can delete alarms from any domain. **Fix:** Change to `self.get_queryset().filter(pk__in=...).delete()` ### 4. Cross-Domain Auto-Reply Creation IDOR (MEDIUM) **File:** `modoboa/autoreply/api/v2/viewsets.py` The `ARMessageViewSet` uses `CreateModelMixin` with `fields = "__all__"` on the serializer. The `mbox` ForeignKey is writable without ownership validation. Any authenticated user can create auto-reply messages for any mailbox by providing its ID. ### 5. Cross-User Calendar Event IDOR (MEDIUM) **File:** `modoboa/calendars/viewsets.py`, lines 90-224 `BaseEventViewSet` and subclasses (`UserEventViewSet`, `SharedEventViewSet`) have no `permission_classes` defined and `get_calendar()` performs no ownership check. Any authenticated user can access, create, modify, and delete events on any calendar. ## Recommended Fixes 1. **Findings 1 & 2:** Change `permissions` to `permission_classes` 2. **Finding 3:** Use `self.get_queryset()` instead of `models.Alarm.objects` 3. **Finding 4:** Add `mbox` ownership validation in serializer 4. **Finding 5:** Add `permission_classes` and filter calendars by user ## Disclosure This report is submitted in good faith. I am available to provide additional details.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/modoboa-modoboa#1941
No description provided.