SEC2-010: The API's iCalendar feeds carry no Cache-Control, which #788's middleware gives only to attachments #952
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#952
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: Data Exposure · Reference: CWE-525 (Use of Web Browser Cache Containing Sensitive Information); OWASP A05:2021 (Security Misconfiguration)
Location:
src/postulo/api/routers/interviews.py:73-83src/postulo/api/routers/reminders.py:42-47src/postulo/core/middleware.py:301-325(keys onContent-Disposition: attachment)src/postulo/core/middleware.py:351-375(keys on a session user andtext/html)src/postulo/api/api.py:886-912(urls(), which already rewrites every pattern's callback)Issue
SECURITY.md(line 30) promises files are delivered withCache-Control: no-store, and #788made that hold for every download by giving the header to any response that is an attachment.
The two API feeds are plain bodies, not attachments, and set nothing themselves:
PrivatePagesAreNotKeptMiddlewaredoes not reach them either: it asks for a session userand
text/html, and a token call has neither. The same is true of every JSON answer of theAPI (applications with their timelines, a company's contacts,
GET /profilewith birth dataand nationalities): no
Cache-Controlat all. A shared cache must not store a response to arequest carrying
Authorizationwithout an explicit directive (RFC 9111 §3.5), which iswhat keeps this Low; a browser's own cache, a browser extension's fetch and some proxies are
under no such rule. The one-interview file at
interviews.py:96-104is an attachment and iscovered.
Impact
Interview dates, places, notes and reminders, and whatever an agent read through the API,
can stay in the HTTP cache of the machine that asked for them, after the token is revoked or
the person signs out, where the web page of the same data cannot.
Fix
Wrap every pattern in
urls()(api.py:886-912, which already rewrites each callback for405s and transactions) so that any API response without a
Cache-Controlof its own getsprivate, max-age=0, no-store;openapi.jsonis a ninja pattern and is covered with the rest.Setting the header on the two feeds alone would keep the
SECURITY.mdsentence true but leavethe JSON answers uncovered. Done when
tests/security/test_downloads_not_cached.pyasserts theheader on
/api/v1/interviews/calendar.ics,/api/v1/reminders/calendar.icsand one JSON list.Related
#788 (closed) - the same promise for the generated downloads; its fix keyed on
Content-Disposition: attachment, which the API feeds and JSON answers do not carry, so thisis the part it did not reach. #787 (closed) - the HTML back/forward-cache half.