fix(sidebar): explicit current-enclave header for sidebar re-renders + membership gate (LC-415) #431

Merged
longjacksonle merged 1 commit from fix/sidebar-enclave-context-hardening into main 2026-06-21 21:20:25 +02:00

Follow-up hardening to LC-415 (#430), covering the whole-sidebar re-render routes that have no authoritative enclave in their own path: star toggle / reorder, mark-all-read, mark-room-read, mark-unread.

Why

The LC-415 fix used the path enclave_id for the category mutations. The routes above can't: a starred or read room may live in a different enclave than the one on screen, so the room's enclave is the wrong context. What the re-render actually needs is the viewer's CURRENT enclave (the one whose sidebar they are looking at), and HX-Current-URL was the only signal for it - fragile when absent, stripped by a proxy, or raced by a sidebar OOB swap. When it failed to resolve, the re-render collapsed to the DM-only sidebar and the just-changed row vanished.

Change

  • live.js sends the viewer's current enclave on every htmx request via a new X-LC-Current-Enclave header, read live from the rendered #sidebar-nav-{id} element id (the element the user is looking at, so always present and correct).
  • New shared resolver routes::current_enclave_for_sidebar prefers that header, falling back to the existing HX-Current-URL / Referer URL parse for non-htmx requests. The two duplicated current_enclave_from_headers copies now delegate to it.
  • Defense-in-depth: the enclave now comes from a client-controlled header (as HX-Current-URL always did), so load_sidebar gates the enclave context on membership. A crafted header naming an enclave the viewer is not in (and is not a site admin of) falls back to the DM-only sidebar instead of leaking that enclave's room + category names. The membership lookup is resolved once and reused for the existing manage check, so it adds no query.

Tests

  • read_all_preserves_enclave_via_explicit_header: the header preserves enclave context through /read-all; the no-header control falls back to DM-only.
  • spoofed_current_enclave_header_does_not_leak_categories: a non-member pointing the header at a foreign enclave cannot enumerate its category names.

just check, just test, just test-saas, node --check live.js all green.

Relates to LC-415.

🤖 Generated with Claude Code

Follow-up hardening to LC-415 (#430), covering the whole-sidebar re-render routes that have no authoritative enclave in their own path: star toggle / reorder, mark-all-read, mark-room-read, mark-unread. ## Why The LC-415 fix used the path `enclave_id` for the category mutations. The routes above can't: a starred or read room may live in a different enclave than the one on screen, so the room's enclave is the wrong context. What the re-render actually needs is the viewer's CURRENT enclave (the one whose sidebar they are looking at), and `HX-Current-URL` was the only signal for it - fragile when absent, stripped by a proxy, or raced by a sidebar OOB swap. When it failed to resolve, the re-render collapsed to the DM-only sidebar and the just-changed row vanished. ## Change - `live.js` sends the viewer's current enclave on every htmx request via a new `X-LC-Current-Enclave` header, read live from the rendered `#sidebar-nav-{id}` element id (the element the user is looking at, so always present and correct). - New shared resolver `routes::current_enclave_for_sidebar` prefers that header, falling back to the existing `HX-Current-URL` / `Referer` URL parse for non-htmx requests. The two duplicated `current_enclave_from_headers` copies now delegate to it. - Defense-in-depth: the enclave now comes from a client-controlled header (as `HX-Current-URL` always did), so `load_sidebar` gates the enclave context on membership. A crafted header naming an enclave the viewer is not in (and is not a site admin of) falls back to the DM-only sidebar instead of leaking that enclave's room + category names. The membership lookup is resolved once and reused for the existing manage check, so it adds no query. ## Tests - `read_all_preserves_enclave_via_explicit_header`: the header preserves enclave context through `/read-all`; the no-header control falls back to DM-only. - `spoofed_current_enclave_header_does_not_leak_categories`: a non-member pointing the header at a foreign enclave cannot enumerate its category names. `just check`, `just test`, `just test-saas`, `node --check live.js` all green. Relates to LC-415. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(sidebar): send viewer's current enclave as an explicit header for sidebar re-renders (LC-415)
All checks were successful
check-secrets / Nosey parker (push) Successful in 4s
check-secrets / Kingfisher (push) Successful in 4s
check-secrets / TruffleHog (push) Successful in 5s
check-secrets / Nosey parker (pull_request) Successful in 9s
check-secrets / Kingfisher (pull_request) Successful in 10s
check-secrets / TruffleHog (pull_request) Successful in 13s
Create release / Create release from merged PR (pull_request) Has been skipped
Check / clippy + fmt + tests (pull_request) Successful in 10m7s
f89b9078cf
Follow-up hardening to LC-415. The whole-sidebar re-render routes that have NO authoritative enclave in their own path - star toggle (`/rooms/{id}/star`), star reorder (`/sidebar/stars/positions`), mark-all-read (`/read-all`), mark-room-read (`/room/{id}/read`), mark-unread (`/messages/{id}/unread`) - still re-derived the viewer's current enclave from `HX-Current-URL`. Unlike the category mutations (fixed in LC-415 by using their path `enclave_id`), these can act on a room in a different enclave than the one on screen, so the room's enclave is the wrong context; the viewer's CURRENT enclave is what the re-render needs, and the header was the only signal for it - fragile when absent, stripped, or raced by a sidebar OOB swap.

live.js now sends the viewer's current enclave on every htmx request via an `X-LC-Current-Enclave` header read live from the rendered `#sidebar-nav-{id}` element id (the very element the user is looking at, so always present and always correct). A new shared resolver `routes::current_enclave_for_sidebar` prefers that header and falls back to the existing `HX-Current-URL` / `Referer` URL parse for non-htmx requests. The two duplicated `current_enclave_from_headers` copies (sidebar_categories.rs, starred_rooms.rs) now delegate to it.

Defense-in-depth: because the enclave now comes from a client-controlled header (as `HX-Current-URL` always was), `load_sidebar` gates the enclave context on membership - a crafted header naming an enclave the viewer is not in (and is not a site admin of) falls back to the DM-only sidebar instead of leaking that enclave's room + category names. The membership lookup is resolved once and reused for the existing manage check, so this adds no query.

Tests: `read_all_preserves_enclave_via_explicit_header` (header preserves context; no-header control falls back) and `spoofed_current_enclave_header_does_not_leak_categories` (non-member cannot enumerate a foreign enclave's categories). `just check`, `just test`, `just test-saas`, `node --check live.js` all green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle deleted branch fix/sidebar-enclave-context-hardening 2026-06-21 21:20:25 +02:00
Sign in to join this conversation.
No reviewers
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
psa-systems/lets-chat!431
No description provided.