BLOCKER: private thumbnail HTTP cache skips Share revocation checks #765

Open
opened 2026-10-02 13:09:59 +00:00 by kayg · 2 comments
Owner

Browser audit follow-up from #663. Audited dev: c4a61e8cf0.

B2 — Private thumbnails enter unmanaged HTTP cache

Status: confirmed policy gap by code trace; duplicate search complete;
filed by sec-browser audit. Merge class: BLOCKER for Share revocation. No claim of
cross-User cookie-cache reuse is made.

crates/plugins/files/src/thumbnails.rs:736 permits versioned thumbnails
with private, max-age=31536000, immutable on both 200 and 304 responses.
It varies on Cookie, which separates changed cookies. That does not cause
revalidation when a Share is revoked with the same cookie. The route checks
Share access before serving, but a fresh browser cache can skip the route.
apps/web/src/lib/appearance/background.svelte.ts:432 uses this variant.
userStorage.clearUser deletes Web Storage, IndexedDB and Cache Storage,
not the browser HTTP cache. Private photo bytes can remain after sign-out.
Round-7a retains the same policy. #449 covers physical server thumbnail
separation, not this browser response policy.

Fix: prevent private bytes from entering unmanaged HTTP cache. Use no-store
and a bounded User cache that cleanup can delete. If HTTP revalidation is
kept for another use, require it on every use and check current access before
304. Cookie variance alone is not a revocation mechanism.

Test: assert the versioned response cannot be reused without an access check.
Use benign synthetic image fixtures for session cleanup and Share revoke.

Evidence is a defensive code trace; no production access or hostile payload was used.

Browser audit follow-up from #663. Audited dev: c4a61e8cf090170f35b1bed3350d9de20c83ecd5. ### B2 — Private thumbnails enter unmanaged HTTP cache Status: confirmed policy gap by code trace; duplicate search complete; filed by sec-browser audit. Merge class: BLOCKER for Share revocation. No claim of cross-User cookie-cache reuse is made. `crates/plugins/files/src/thumbnails.rs:736` permits versioned thumbnails with `private, max-age=31536000, immutable` on both 200 and 304 responses. It varies on Cookie, which separates changed cookies. That does not cause revalidation when a Share is revoked with the same cookie. The route checks Share access before serving, but a fresh browser cache can skip the route. `apps/web/src/lib/appearance/background.svelte.ts:432` uses this variant. `userStorage.clearUser` deletes Web Storage, IndexedDB and Cache Storage, not the browser HTTP cache. Private photo bytes can remain after sign-out. Round-7a retains the same policy. #449 covers physical server thumbnail separation, not this browser response policy. Fix: prevent private bytes from entering unmanaged HTTP cache. Use no-store and a bounded User cache that cleanup can delete. If HTTP revalidation is kept for another use, require it on every use and check current access before 304. Cookie variance alone is not a revocation mechanism. Test: assert the versioned response cannot be reused without an access check. Use benign synthetic image fixtures for session cleanup and Share revoke. Evidence is a defensive code trace; no production access or hostile payload was used.
Author
Owner

Code trace confirms the versioned private thumbnail path still returns Cache-Control: private, max-age=31536000, immutable and can answer 304. The route checks the Share before that response, but a fresh browser cache can skip the route after revoke. I am changing the private thumbnail response to private, no-store and adding a regression check for conditional requests.

Code trace confirms the versioned private thumbnail path still returns `Cache-Control: private, max-age=31536000, immutable` and can answer 304. The route checks the Share before that response, but a fresh browser cache can skip the route after revoke. I am changing the private thumbnail response to `private, no-store` and adding a regression check for conditional requests.
Author
Owner

The thumbnail response is now private, no-store for both legacy and current URLs. I also found that changing response headers does not evict a fresh v=1 response already stored by the browser under the earlier one-year immutable policy. New Appearance URLs now use v=2 so they reach the authorization route; v=1 remains accepted with no-store for compatibility. Updated regressions assert the new URL and response policy.

The thumbnail response is now private, no-store for both legacy and current URLs. I also found that changing response headers does not evict a fresh v=1 response already stored by the browser under the earlier one-year immutable policy. New Appearance URLs now use v=2 so they reach the authorization route; v=1 remains accepted with no-store for compatibility. Updated regressions assert the new URL and response policy.
Sign in to join this conversation.
No labels
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
kayg/calternal#765
No description provided.