SEC2-002: Deactivating an account and changing someone's username under *People* need no fresh proof, unlike the neighbouring actions #944

Closed
opened 2026-10-17 10:37:44 +00:00 by tiagoagueda · 0 comments
Owner

Found on 2026-10-16 in a read-only security review of postulo at 56fc9d531, checked by a second, independent pass.

Severity: Low · Category: Authentication/Authorization · Reference: CWE-306 (Missing Authentication for Critical Function); OWASP A07:2021 (Identification and Authentication Failures)

Location:

  • src/postulo/core/server_views.py:280-302 (PersonActiveView.post)
  • src/postulo/core/server_views.py:438-477 (PersonUsernameView)
  • src/postulo/accounts/reauth.py:42-50 (reauthentication_required, with its allow_get option)

Issue

#740 put reauthentication_required on appointing/demoting an administrator (line 245), deleting an account (line 334) and issuing a recovery link (line 384). Two actions on the same page were left out:

class PersonActiveView(StaffRequiredMixin, View):
    def post(self, request: HttpRequest, pk: int) -> HttpResponse:   # no decorator
        person = get_object_or_404(get_user_model(), pk=pk)
        ...
                person.is_active = False
class PersonUsernameView(StaffRequiredMixin, UpdateView):            # no decorator
    def get_queryset(self):
        return get_user_model().objects.all()

Deactivation ends every session of the target (ModelBackend.get_user refuses an inactive user) and refuses their API tokens (api/auth.py:70), so it is a denial of access to a member; a username change alters a sign-in identifier and what every other member sees the person as. The only guard is a session that is signed in as staff, which is exactly the left-open-browser case #740 was about.

Impact

Someone with an administrator's unattended or stolen session can lock any member out (deactivate) or rename them, including the operator, without being asked for a password or second factor. Reversible, so Low; but it is the same precondition and page as #740.

Fix

Add @method_decorator(reauthentication_required) to PersonActiveView.post. PersonUsernameView is an UpdateView, so decorate its dispatch with reauthentication_required(allow_get=True) (the project's wrapper supports allow_get, accounts/reauth.py:50) or override post. Done when tests/test_hardening_740.py gains the two routes server:person_active and server:person_username beside the existing three, showing that an administrator whose last authentication is older than ACCOUNT_REAUTHENTICATION_TIMEOUT is sent to reauthenticate and nothing changed.

#740 (closed) - covered demotion, deletion and recovery links; these two actions on the same page were not included, so this is an incomplete fix. #741 (closed) - SSO freshness, already handled by the wrapper.

Found on 2026-10-16 in a read-only security review of `postulo` at `56fc9d531`, checked by a second, independent pass. **Severity:** Low · **Category:** Authentication/Authorization · **Reference:** CWE-306 (Missing Authentication for Critical Function); OWASP A07:2021 (Identification and Authentication Failures) **Location:** - `src/postulo/core/server_views.py:280-302` (`PersonActiveView.post`) - `src/postulo/core/server_views.py:438-477` (`PersonUsernameView`) - `src/postulo/accounts/reauth.py:42-50` (`reauthentication_required`, with its `allow_get` option) ## Issue #740 put `reauthentication_required` on appointing/demoting an administrator (line 245), deleting an account (line 334) and issuing a recovery link (line 384). Two actions on the same page were left out: ```python class PersonActiveView(StaffRequiredMixin, View): def post(self, request: HttpRequest, pk: int) -> HttpResponse: # no decorator person = get_object_or_404(get_user_model(), pk=pk) ... person.is_active = False ``` ```python class PersonUsernameView(StaffRequiredMixin, UpdateView): # no decorator def get_queryset(self): return get_user_model().objects.all() ``` Deactivation ends every session of the target (`ModelBackend.get_user` refuses an inactive user) and refuses their API tokens (`api/auth.py:70`), so it is a denial of access to a member; a username change alters a sign-in identifier and what every other member sees the person as. The only guard is a session that is signed in as staff, which is exactly the left-open-browser case #740 was about. ## Impact Someone with an administrator's unattended or stolen session can lock any member out (deactivate) or rename them, including the operator, without being asked for a password or second factor. Reversible, so Low; but it is the same precondition and page as #740. ## Fix Add `@method_decorator(reauthentication_required)` to `PersonActiveView.post`. `PersonUsernameView` is an `UpdateView`, so decorate its `dispatch` with `reauthentication_required(allow_get=True)` (the project's wrapper supports `allow_get`, `accounts/reauth.py:50`) or override `post`. Done when `tests/test_hardening_740.py` gains the two routes `server:person_active` and `server:person_username` beside the existing three, showing that an administrator whose last authentication is older than `ACCOUNT_REAUTHENTICATION_TIMEOUT` is sent to reauthenticate and nothing changed. ## Related #740 (closed) - covered demotion, deletion and recovery links; these two actions on the same page were not included, so this is an incomplete fix. #741 (closed) - SSO freshness, already handled by the wrapper.
tiagoagueda added this to the 0.6.0 milestone 2026-10-17 10:37:45 +00:00
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
Postulo/postulo#944
No description provided.