PST-063: Any administrator can strip the operator's superuser flag through People #792

Closed
opened 2026-10-10 10:56:34 +00:00 by tiagoagueda · 1 comment
Owner

Found in the security audit of 2026-10-10 (SECURITY-AUDIT-2026-10-10.md), run against main at c30851b from 8 to 10 October 2026, as PST-063. Severity: Informational.

The text below is the audit's entry, unchanged apart from links to the other issues it names. The finding was produced by automated review and checked by a second automated pass; re-check it before acting on it.


  • Severity: Informational
  • Category: privilege-management
  • Location: src/postulo/core/server_views.py (236-259)
  • Found by: ownership review of core
  • Related: PST-011 (#740)

Who can do it, and what they need. A signed-in administrator (is_staff) who is not the operator; the operator must not be the last active administrator.

How it works.

  1. Administrator A opens /server/people/.
  2. A posts to /server/people/<operator pk>/administrator/ (the 'Remove administrator' menu item).
  3. PersonAdminView.post sees person.is_staff is True and, because A exists as another administrator, _last_administrator() is False, so it sets person.is_staff = False AND person.is_superuser = False and saves.
  4. Re-promoting (a second POST) sets is_staff only, so the superuser flag is gone from the interface for good. Verified with a scratch test ([scratch test]): a staff account demoted a superuser operator and the re-promotion left is_superuser False.

Impact. A lower-privileged administrator can permanently remove the operator's superuser standing (the flag docs/THREAT-MODEL.md and admin_site.py reserve for 'the operator's own account'), locking the operator out of a mounted Django admin and out of every superuser-only path, and can do so before the operator notices. …

Verification (verdict: confirmed; severity after verification: Informational; first-pass claim: Low). Precondition unchanged (a second active staff administrator). Impact corrected: the is_superuser flag is read nowhere in Postulo and matters only for a Django admin the operator chose to mount; the demotion itself (is_staff) is a documented power every administrator has over every other. The loss is irreversibility from the UI, recoverable at the console (manage.py shell / createsuperuser, as the #368 changelog entry already instructs). This is a deliberate choice tested in tests/test_server_settings.py:226-231, and the actor is one the threat model trusts, so it is an observation rather than a vulnerability.

Suggested remediation. Refuse to demote an account that holds is_superuser from the People page unless the acting administrator is also a superuser (or leave is_superuser untouched when demoting, as the promotion path already leaves it alone), log the change with who did it, and add a test in tests/security/test_staff_only.py that a plain staff account cannot change another account's is_superuser.

Found in the security audit of 2026-10-10 (`SECURITY-AUDIT-2026-10-10.md`), run against `main` at `c30851b` from 8 to 10 October 2026, as **PST-063**. Severity: **Informational**. The text below is the audit's entry, unchanged apart from links to the other issues it names. The finding was produced by automated review and checked by a second automated pass; re-check it before acting on it. --- - **Severity:** Informational - **Category:** privilege-management - **Location:** `src/postulo/core/server_views.py` (236-259) - **Found by:** ownership review of core - **Related:** PST-011 (#740) **Who can do it, and what they need.** A signed-in administrator (is_staff) who is not the operator; the operator must not be the last active administrator. **How it works.** 1. Administrator A opens /server/people/. 2. A posts to /server/people/&lt;operator pk>/administrator/ (the 'Remove administrator' menu item). 3. PersonAdminView.post sees person.is_staff is True and, because A exists as another administrator, _last_administrator() is False, so it sets person.is_staff = False AND person.is_superuser = False and saves. 4. Re-promoting (a second POST) sets is_staff only, so the superuser flag is gone from the interface for good. Verified with a scratch test ([scratch test]): a staff account demoted a superuser operator and the re-promotion left is_superuser False. **Impact.** A lower-privileged administrator can permanently remove the operator's superuser standing (the flag docs/THREAT-MODEL.md and admin_site.py reserve for 'the operator's own account'), locking the operator out of a mounted Django admin and out of every superuser-only path, and can do so before the operator notices. … **Verification** (verdict: confirmed; severity after verification: Informational; first-pass claim: Low). Precondition unchanged (a second active staff administrator). Impact corrected: the is_superuser flag is read nowhere in Postulo and matters only for a Django admin the operator chose to mount; the demotion itself (is_staff) is a documented power every administrator has over every other. The loss is irreversibility from the UI, recoverable at the console (`manage.py shell` / `createsuperuser`, as the #368 changelog entry already instructs). This is a deliberate choice tested in tests/test_server_settings.py:226-231, and the actor is one the threat model trusts, so it is an observation rather than a vulnerability. **Suggested remediation.** Refuse to demote an account that holds is_superuser from the People page unless the acting administrator is also a superuser (or leave is_superuser untouched when demoting, as the promotion path already leaves it alone), log the change with who did it, and add a test in tests/security/test_staff_only.py that a plain staff account cannot change another account's is_superuser.
tiagoagueda added this to the 1.0.0 milestone 2026-10-10 10:58:01 +00:00
Author
Owner

Fixed in 9b7e0ce2d on main.

Fixed in 9b7e0ce2d on main.
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#792
No description provided.