PST-059: Generated downloads (calendar, report CSV and PDF, contact export) are sent without Cache-Control: no-store #788

Closed
opened 2026-10-10 10:56:31 +00:00 by tiagoagueda · 0 comments
Owner

Found in the security audit of 2026-10-10 (SECURITY-AUDIT-2026-10-10.md), run against main at c30851b from 8 to 10 October 2026, as PST-059. Severity: Low.

The text below is the audit's entry, unchanged apart from links to the other issues it names. The finding was produced by automated review and checked by a second automated pass; re-check it before acting on it.


  • Severity: Low
  • Category: information-disclosure / caching
  • Location: src/postulo/applications/views.py (1220-1253); src/postulo/jobs/views.py (1021-1046)
  • Found by: completeness review: tests against claims, ownership review of jobs

Who can do it, and what they need. Whoever next uses the same browser profile or disk cache (shared or family computer, library machine), or an operator-installed caching proxy that applies heuristic caching. Needs no account; the victim must have downloaded one of these files while signed in.

How it works.

  1. The signed-in person downloads the interview calendar (/applications/interviews/calendar.ics or /applications/interviews/<pk>/calendar.ics), the report CSV (/applications/report/report.csv) or the report PDF.
  2. The responses carry Content-Disposition but no Cache-Control and no validators, so the browser may keep them in its HTTP cache.
  3. After sign-out, someone with access to the profile's cache reads the stored response. In addition the avatar and company-logo views send private, max-age=86400, so a picture that an administrator fetched for any member stays cached for a day.

Impact. Interview dates, company names, contacts' names and the application evidence list can remain in a cache that SECURITY.md says files never enter ("Files are delivered ... with Cache-Control: no-store"). Narrow precondition, so low. Sibling downloads (reminder feed, calendar_download, vCards, kind files, candidate file, uploads, rendered documents) do set no-store, which shows the omission is an oversight rather than a decision. Contact data-subject export lacking it is already on the list; these are different endpoints.

Evidence.

Observed with a scratch test that GETs each download as a signed-in user: applications:interview_calendar -> Cache-Control None; applications:interview_ics -> None; applications:report_csv -> None; API /api/v1/interviews/calendar.ics and /api/v1/reminders/calendar.ics -> None; accounts:avatar and jobs:company_logo -> 'private, max-age=86400'. Code: applications/views.py:1251-1252 `response = HttpResponse(text, content_type="text/calendar; charset=utf-8")` / `response["Content-Disposition"] = f'attachment; filename="{filename}"'` and 1357-1359 (CSV), 1414-1416 (PDF) with no Cache-Control; contrast applications/views_ical.py:27 `response["Cache-Control"] = "private, max-age=0, no-store"` and c
…

Verification (verdict: confirmed; severity after verification: Low). Keep the five routes (the two interview .ics endpoints, report CSV, report PDF GET, and the three API calendar feeds), and drop the avatar and logo sentence because it is deliberate and test-pinned. Line corrections: ReportCSVView.get is about views.py:1351-1359 and ReportPDFView.get about 1402-1416. The attacker needs the victim's browser profile or disk cache, which the threat model excludes, and the identical data is cached just as freely in HTML pages. Treat it as a consistency fix against SECURITY.md (one shared helper that sets private, max-age=0, no-store on generated downloads, plus a test that walks the download routes) rather than a vulnerability.

Suggested remediation. Set Cache-Control: private, max-age=0, no-store on every authenticated response that carries a download (a small middleware keyed on Content-Disposition: attachment, or never_cache on these views and the API calendar feeds), serve avatars and logos with no-cache or a short private lifetime, and add a test that iterates every download route and asserts the header.

Also covered by this issue.

  • Contact data-subject export is answered without Cache-Control: no-store (src/postulo/jobs/views.py (1021-1046); Informational). Narrow: a copy of one contact's personal-data document may persist in a cache after the session. SECURITY.md promises files are delivered 'with Cache-Control: no-store'.
Found in the security audit of 2026-10-10 (`SECURITY-AUDIT-2026-10-10.md`), run against `main` at `c30851b` from 8 to 10 October 2026, as **PST-059**. Severity: **Low**. The text below is the audit's entry, unchanged apart from links to the other issues it names. The finding was produced by automated review and checked by a second automated pass; re-check it before acting on it. --- - **Severity:** Low - **Category:** information-disclosure / caching - **Location:** `src/postulo/applications/views.py` (1220-1253); `src/postulo/jobs/views.py` (1021-1046) - **Found by:** completeness review: tests against claims, ownership review of jobs **Who can do it, and what they need.** Whoever next uses the same browser profile or disk cache (shared or family computer, library machine), or an operator-installed caching proxy that applies heuristic caching. Needs no account; the victim must have downloaded one of these files while signed in. **How it works.** 1. The signed-in person downloads the interview calendar (/applications/interviews/calendar.ics or /applications/interviews/&lt;pk>/calendar.ics), the report CSV (/applications/report/report.csv) or the report PDF. 2. The responses carry Content-Disposition but no Cache-Control and no validators, so the browser may keep them in its HTTP cache. 3. After sign-out, someone with access to the profile's cache reads the stored response. In addition the avatar and company-logo views send `private, max-age=86400`, so a picture that an administrator fetched for any member stays cached for a day. **Impact.** Interview dates, company names, contacts' names and the application evidence list can remain in a cache that SECURITY.md says files never enter ("Files are delivered ... with Cache-Control: no-store"). Narrow precondition, so low. Sibling downloads (reminder feed, calendar_download, vCards, kind files, candidate file, uploads, rendered documents) do set no-store, which shows the omission is an oversight rather than a decision. Contact data-subject export lacking it is already on the list; these are different endpoints. **Evidence.** ```text Observed with a scratch test that GETs each download as a signed-in user: applications:interview_calendar -> Cache-Control None; applications:interview_ics -> None; applications:report_csv -> None; API /api/v1/interviews/calendar.ics and /api/v1/reminders/calendar.ics -> None; accounts:avatar and jobs:company_logo -> 'private, max-age=86400'. Code: applications/views.py:1251-1252 `response = HttpResponse(text, content_type="text/calendar; charset=utf-8")` / `response["Content-Disposition"] = f'attachment; filename="{filename}"'` and 1357-1359 (CSV), 1414-1416 (PDF) with no Cache-Control; contrast applications/views_ical.py:27 `response["Cache-Control"] = "private, max-age=0, no-store"` and c … ``` **Verification** (verdict: confirmed; severity after verification: Low). Keep the five routes (the two interview .ics endpoints, report CSV, report PDF GET, and the three API calendar feeds), and drop the avatar and logo sentence because it is deliberate and test-pinned. Line corrections: ReportCSVView.get is about views.py:1351-1359 and ReportPDFView.get about 1402-1416. The attacker needs the victim's browser profile or disk cache, which the threat model excludes, and the identical data is cached just as freely in HTML pages. Treat it as a consistency fix against SECURITY.md (one shared helper that sets `private, max-age=0, no-store` on generated downloads, plus a test that walks the download routes) rather than a vulnerability. **Suggested remediation.** Set `Cache-Control: private, max-age=0, no-store` on every authenticated response that carries a download (a small middleware keyed on Content-Disposition: attachment, or `never_cache` on these views and the API calendar feeds), serve avatars and logos with `no-cache` or a short private lifetime, and add a test that iterates every download route and asserts the header. **Also covered by this issue.** - **Contact data-subject export is answered without Cache-Control: no-store** (`src/postulo/jobs/views.py` (1021-1046); Informational). Narrow: a copy of one contact's personal-data document may persist in a cache after the session. SECURITY.md promises files are delivered 'with Cache-Control: no-store'.
tiagoagueda added this to the 0.6.0 milestone 2026-10-10 10:57:58 +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#788
No description provided.