SEC2-029: A plugin's templates/ directory is searched before every app-directory template, contrary to what themes.py promises #971

Closed
opened 2026-10-17 10:38:46 +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: 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_dir appends to TEMPLATES[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 DIRS before the app-directories loader runs, so a plugin's templates/ directory comes after Postulo's own templates/ (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 is TemplatesSetting), allauth's account/*.html where Postulo has not overridden them, django_tasks_db's and the admin's. A plugin that declares a theme and ships templates/django/forms/widgets/input.html redraws 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 have documents.themes resolve 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 holding django/forms/widgets/input.html and asserts a rendered form does not use it.

#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.

Found on 2026-10-16 in a read-only security review of `postulo` at `56fc9d531`, 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_dir` appends to `TEMPLATES[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 `DIRS` before the app-directories loader runs, so a plugin's `templates/` directory comes after Postulo's own `templates/` (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 is `TemplatesSetting`), allauth's `account/*.html` where Postulo has not overridden them, `django_tasks_db`'s and the admin's. A plugin that declares a theme and ships `templates/django/forms/widgets/input.html` redraws 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 have `documents.themes` resolve 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 holding `django/forms/widgets/input.html` and 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.
tiagoagueda added this to the 1.0.0 milestone 2026-10-17 10:38:47 +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#971
No description provided.