You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: docs/threat-model.md
+109Lines changed: 109 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -160,6 +160,115 @@ Same matrix as list with these additions:
160
160
| I | The injected `<meta name="dar-mount">` leaks more than the mount | The view sets exactly one meta tag with the resolved mount string (escaped). | S-29 |
161
161
| E | T-4 reads `csrftoken` cookie via JS to issue forged requests |`csrftoken` is not `HttpOnly` (by design — the SPA needs it). Mitigation is upstream CSP + same-site cookies. | S-65 |
162
162
163
+
> **Status note (2026-05-27 refresh).** §4.1–4.6 above were written
164
+
> pre-merge ("lands in PR #N"); all six are now on `main` and tested.
165
+
> The endpoint surface has since grown — §4.7–4.16 below STRIDE the
| S | T-1 username enumeration via differential responses/timing | One generic `403 invalid_credentials` for unknown-user / wrong-password / inactive / non-staff. Django's `ModelBackend` runs the hasher even for unknown users (no timing oracle). |`test_auth.py::test_unknown_user_returns_generic_403`, `…wrong_password…`|
177
+
| S | T-4 CSRF-forges a login/logout from another origin | CSRF enforced (no `@csrf_exempt`); shell sets the cookie. |`test_auth.py::test_login_without_csrf_is_403`, `…logout_without_csrf…`|
178
+
| E | T-2 (valid non-staff) gains a staff session | Access policy (`is_admin_user`) runs **before**`login()`; a valid-but-unauthorized user gets **no** session cookie. |`test_auth.py::test_valid_nonstaff_user_returns_generic_403_no_session`|
179
+
| S | Session fixation — reuse a pre-login session id |`django.contrib.auth.login` rotates the session key. | (Django built-in) |
180
+
| I | Password leaks into logs/response | Password is read from the body and passed straight to `authenticate`; never logged or echoed. | code review |
181
+
| D | T-1 brute-forces credentials | Out of scope (consumer's job) — documented `django-ratelimit`/`django-axes` recommendation. QSEC-01. | n/a |
182
+
| — | Shell served to anon under `REACT_LOGIN`| Shell carries no user data; every data API call still 403s until auth. |`test_spa_index.py::test_react_login_on_anon_gets_shell_not_redirect`|
| I | Anonymous manifest leaks per-user data | Manifest is computed at request time from mount + AdminSite header + static fields — **no** user data. |`test_pwa.py::test_manifest_carries_no_per_user_data`|
267
+
| T | SW claims scope beyond the mount (intercepts sibling Django views) | Served with `Service-Worker-Allowed: <mount>`; SW `fetch` passes through anything outside the mount. |`test_pwa.py::test_sw_served_with_scope_header`|
268
+
| I | SW caches a `no-store` API read → payload outlives the session | SW skips caching any response whose `Cache-Control` includes `no-store`; **cache-purge on logout** (`dar:purge`). |`test_pwa.py::test_sw_embeds_mount_and_security_guards`|
0 commit comments