fix(thumbnails): private, no-cache — the URL is gated and mutable
Thumbnails were served `public, max-age=31536000, immutable`. Two problems, and the first is a security one. `public` on a Permission::Read gated resource lets any shared cache — a corporate proxy, a CDN — store one user's thumbnail and serve it to another. `Vary: Accept` was no defence: it does not vary on Authorization. Now `private`. `immutable` was a promise this URL cannot keep. It is keyed by file id, and its bytes change when a preview is uploaded, when content is replaced, or when an attachment is removed. `immutable` tells a client not to revalidate at all during the freshness lifetime, so with a one-year max-age a browser that fetched once would never see a new preview — which also made the content-keyed ETag unobservable in practice. A correct validator is worthless if nothing asks. Now `no-cache`, which still stores the body and only requires revalidation, answered by the ETag with a body-less 304. The hurl tests could not have caught this: hurl always sends the request, so If-None-Match was exercised and passed while a browser obeying `immutable` never got that far. Same "correct on the wire, wrong in practice" shape as the bugs before it, so the test now asserts the directives themselves rather than only the 304 behaviour. One definition, shared by the REST and NextCloud endpoints, which are gated identically and must not drift. /_app/immutable is untouched: those are hash-named static assets, genuinely content-addressed and public, where the directive is honest. Cost is a conditional request per thumbnail per page load. Recovering it needs a content-addressed URL — where `immutable` would be true — but that puts the hash in the URL of an authorized resource, so it stays `private` regardless, and it touches the SPA and the file DTO. Separate change.
This commit is contained in:
@@ -82,7 +82,15 @@ HTTP 200
|
||||
[Captures]
|
||||
etag_before: header "ETag"
|
||||
[Asserts]
|
||||
header "Cache-Control" contains "immutable"
|
||||
# `private`, because a thumbnail is Permission::Read gated — `public` let a
|
||||
# shared proxy hand one user's thumbnail to another. `no-cache` rather than
|
||||
# `immutable`, because this URL is keyed by file id and its bytes change
|
||||
# when content is replaced or a preview uploaded; `immutable` suppressed
|
||||
# revalidation entirely, which made the ETag below unobservable in a real
|
||||
# client.
|
||||
header "Cache-Control" contains "private"
|
||||
header "Cache-Control" contains "no-cache"
|
||||
header "Cache-Control" not contains "immutable"
|
||||
|
||||
|
||||
# Unchanged content revalidates to 304 — the caching path works.
|
||||
|
||||
Reference in New Issue
Block a user