SEC2-025: A plugin's settings page is not held to the access list's "may change its settings": only the button is #967
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
performance
security
tier
1
tier
2
tier
3
tier/4
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Postulo/postulo#967
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Found on 2026-10-16 in a read-only security review of
postuloat56fc9d531, checked by a second, independent pass.Severity: Low · Category: Authentication/Authorization · Reference: CWE-285 (Improper Authorization); CWE-602 (Client-Side Enforcement of Server-Side Security); OWASP A01:2021 (Broken Access Control)
Location:
src/postulo/plugins/policy.py:173-186(may_change_settings, called nowhere but the plugins list)src/postulo/core/server_views.py:1317,:1361("settings_allowed": policy.may_change_settings(user, plugin.name)feeds a disabled button only)src/postulo/core/server_views.py:480-504(PolicyView(ServerSectionMixin, UpdateView):StaffRequiredMixinand nothing else)src/postulo/core/server_views.py:618,:688,:785,:1132(MapsView,DataProtectionView,EmailView,IdentifierSchemesView: the pages the manifests name insettings=)src/postulo/plugins/models.py:195-214(may_change_settings: "the right to use the plugin's settings page, for an administrator who is not the operator")src/postulo/core/views_access.py:41(GroupsView: decides group membership,StaffRequiredMixinonly)Issue
#886 added
PluginAccess.may_change_settings, documented on the model as "the right to use the plugin's settings page, for an administrator who is not the operator", andpolicy.may_change_settings()implements the decision (operator always; another administrator unless a rule for them or their groups says no). The only caller is_policy_rows, which uses it to draw the Settings button disabled:The pages the button leads to (
server:maps,server:data_protection,server:email,server:identifier_schemes, named in the manifests ofmaps,gdpr,email/smtpandidentifiers) are plainPolicyViewsubclasses whose only guard isStaffRequiredMixin.test_func(is_staff). An administrator whose rule saysmay_change_settings=Falseopens the address directly, or posts the form, and the setting is saved;PolicyView.form_validrecords them asupdated_by, so the change is at least attributed.tests/test_plugin_access.py:135tests only that the button is disabled.The same holds one level up: the access-list page (
PluginAccessView) andGroupsVieware themselves guarded byis_staffonly, so an excluded administrator could also lift the rule, or change the group membership it keys on, before using the page.Impact
An administrator the operator deliberately excluded from a plugin's settings changes them anyway: the map tile server every member's browser fetches from (and so where the neighbourhood of their search is sent), the retention policy and privacy notice, the identifier schemes, and the instance's mail settings page, which holds the mail password and the OAuth client secret and is where the recovery mail of every account is configured. The restriction exists only in the interface.
Fix
Enforce the rule in the view. Give
PolicyView(or a mixin the settings views use) the plugin's name and refuse indispatch:Prefer deriving the gate from
manifest.settings(resolve each manifest's settings URL name to its view) over a hand-keptplugin_nameper view, so a future plugin page is covered automatically. Whichever form, the gate has to coverEmailConsentView,EmailTestViewandEmailConnectionTestView(reached from the mail page) and the htmx dialog path ofIdentifierSchemesView, not only the four main views. For the check to mean anything, the access-list page andGroupsViewalso need a rule the excluded administrator cannot undo: at least "an administrator may not edit a rule about themselves or their own groups", or operator-only.Done when: a test in
tests/test_plugin_access.pymakes a staff account with amay_change_settings=Falserule formaps, GETs and POSTsserver:maps, and asserts 403 and an unchangedSiteSettings; and a sibling test asserts the same account cannot remove its own rule through the access-list page.Related
#886 (closed) - introduced the right and says the Settings button's "not authorised" state reads it; it does not say the page itself is gated, and the code gates only the button, so this is an incomplete fix of #886. #885 (closed) - the button. #368 (closed) - the superuser grant; neither covers the server-side check.