SEC2-029: A plugin's templates/ directory is searched before every app-directory template, contrary to what themes.py promises #971
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#971
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: Informational · Category: Configuration · Reference: CWE-427 (Uncontrolled Search Path Element); OWASP A05:2021 (Security Misconfiguration)
Location:
src/postulo/plugins/themes.py:15-19("a plugin cannot replace a page of Postulo's interface by shipping a file at the same path")src/postulo/plugins/themes.py:52-68(register_template_dirappends toTEMPLATES[0]["DIRS"])src/postulo/config/settings/base.py:134-138(DIRS: [PACKAGE_DIR / "templates"],APP_DIRS: True; django-cotton 2.7.2 rewrites this into[cotton, filesystem, app_directories]under the cached loader)src/postulo/config/settings/base.py:230(FORM_RENDERER = "django.forms.renderers.TemplatesSetting": widget templates are resolved by the project engine)Issue
The filesystem loader walks
DIRSbefore the app-directories loader runs, so a plugin'stemplates/directory comes after Postulo's owntemplates/(which it cannot shadow) but before every template Django finds in an installed app:django/forms/widgets/*.html(every form control on the instance, since the renderer isTemplatesSetting), allauth'saccount/*.htmlwhere Postulo has not overridden them,django_tasks_db's and the admin's. A plugin that declares a theme and shipstemplates/django/forms/widgets/input.htmlredraws every input on the instance. The docstring's claim holds only for Postulo's own directory.Impact
None beyond what an installed plugin can already do in-process (the threat model calls this a trust boundary, not a sandbox); it is recorded because the module states a guarantee the loader order does not give, and an administrator reading it would believe a theme plugin can touch documents only.
Fix
Mount plugin templates under a namespace the renderer looks up explicitly: register the directory under a dedicated engine, or require theme templates at
<plugin-name>/...and havedocuments.themesresolve them by that prefix, so a plugin's directory is never on the path ordinary pages are drawn from. That is the option that makes the docstring true; merely correcting the docstring is the fallback. Done when: a test registers a plugin template directory holdingdjango/forms/widgets/input.htmland asserts a rendered form does not use it.Related
#820 (closed) - records that plugin themes are handed full model instances, with the same "trust boundary" verdict; this is a documentation and ordering gap beside it, not a duplicate.