Security tests and hardening lag the code: no isolation sweep, CSP untested, invites unhashed, proxy trust too wide, mail to any address #232
Labels
No labels
accessibility
authentication
breaking change
bug
documentation
enhancement
interface
internationalisation
observability
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#232
Loading…
Add table
Add a link
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?
Owner scoping is strong in practice: every view, form queryset, API router, search and export read in the audit narrows with
for_user(). The tests and a few defaults have not kept up. Found in the 2026-09-15 code audit.Tests
The isolation sweep the threat model promises does not exist.
THREAT-MODEL.md:14says "the test suite sweeps every view and queryset for this".tests/test_ownership.pytests only the mixin against a test model, and isolation is otherwise tested app by app, so a new view with a pk can ship untested.→
tests/security/test_isolation_sweep.py: walk the resolver (astest_page_coverage.pydoes), map each pattern with a pk to a factory that creates the object for another user, and assert GET and POST return 404. Keep anEXCUSEDdict with reasons. Do the same for the API routes.tests/security/misses recent boundaries. Nothing covers:accounts/views.py:215-217);export_download;Rule 7 (
THREAT-MODEL.md:75) asks for one test per endpoint.→
test_feeds_and_pictures.pyfor cross-owner ICS, calendar, avatar and logo requests. Outbound tests are in the outbound-requests issue (#215).The CSP exists only in production settings.
SECURE_CSPis defined only inconfig/settings/prod.py:77-87;test.pyanddev.pyhave none, andtests/security/test_requests.py:146only reads the settings dict. The e2e and axe suite runs without the policy, so an inline script orstyle=attribute passes CI and breaks in production.→ Move
SECURE_CSPintobase.py, have the e2e suite fail onsecuritypolicyviolationevents, and addworker-src 'self'andobject-src 'none'explicitly now that/sw.jsexists.Hardening
accounts/models.py:455-472, looked up directly ataccounts/views.py:295), while recovery links store only a SHA-256 fingerprint and API tokens are hashed. This goes against rule 6 and the backup row of the threat model: anyone with a database backup can register with a pending invite, which also counts as proof of the email address.→ Store a fingerprint and show the link once at creation, as recovery links do. Existing pending invites need re-issuing, and
invite_list.html:48must stop showing the link later.X-Forwarded-Forby default.core/proxy.py:37-45trusts 10/8, 172.16/12, 192.168/16, fc00::/7 and fe80::/10, and rewritesREMOTE_ADDR. allauth's rate limits, the admin login limit andPOSTULO_ENDPOINT_RATEall key onREMOTE_ADDR. Any LAN host, or a container on the same bridge, picks its own rate-limit identity. Under rootless Docker or Podman every internet client may arrive from a private gateway address (unconfirmed).→ Default to loopback, have the operator name the proxy network, and show on Server settings → Overview which peer was trusted.
plugins/email/__init__.py:55-63sends toconfig["to"], a plain email field with no check that the address is the person's, and the subject carries text the person controls.ConnectionTestViewhas no throttle; the text transport has per-account limits and mail has none.→ Limit
toto the account's verified addresses (allauth), or confirm a foreign address first, and throttle Test.Landed on
mainin six commits, one per point:tests/security/test_isolation_sweep.pywalks the resolver and, for every address that names a record (69 pages, 19 API routes), makes the other person's record and asks for it: GET and POST must be 404 or 405, an API route 404 with a body its schema accepts. A new pattern with an argument fails until it has a factory or a written excuse. Every existing one answers 404. The threat model now names the file instead of asserting the sweep exists.tests/security/test_feeds_and_pictures.py: the calendar in its three shapes, both iCalendar feeds and the API's, the avatar rule, a company's logo, and the export archive (a POST, own records only).SECURE_CSPis inbase.py, so every settings module sends it;worker-src,manifest-srcandobject-srcare named. The browser suite runs under it and an autouse fixture fails any test on a policy refusal; axe goes in through the DevTools protocol and the text-spacing override through a constructed stylesheet, since both were<script>/<style>elements with content.Invite.issuehands the token back once, to the page that made it, which shows the link with the Copy button; the list has no link column; the session holds the fingerprint. The migration fingerprints existing tokens, so links already sent still open.POSTULO_TRUSTED_PROXIESnames the network; Server settings → Overview shows the peer of the current request and whether it was trusted. The changelog says in bold what an upgrade behind a container or another host must set.tois a choice among the person's verified addresses (primary first);sendandtestboth resolve the recipient the same way and refuse an address that stopped being theirs; a title is put on one line before it becomes a subject; Test on a connection and on the mail settings page is bounded byPOSTULO_CONNECTION_TEST_RATE(10/h). Postulo handsuserto aconfig_fieldsortestthat asks for it by keyword, so the plugin contract is unchanged.