From 69a08327d24d08ba7a4e879116eeff60ac2a3874 Mon Sep 17 00:00:00 2001 From: tv Date: Sun, 8 Nov 2026 14:13:05 +0000 Subject: dashboard: hand over a token reading the member list only Review finding 8, second part: opening another member's dashboard handed it a token reading everything (READ_ALL). A member writes its own dashboard URL, so a malicious one received a read-everything token under the operator's identity: /admin/config, /admin/changes, /admin/log and the rest, on every node the user reaches. The token handed over now reads the member list only (READ_MEMBERS, as sent to subscribed nodes), which is enough to draw the map; the user logs in at the other dashboard to see more. With 1151 it lasts five minutes and isn't refreshed past them. --- docs/access.md | 2 +- docs/dashboard.md | 9 +++++---- src/api/dashboard/dashboard.html | 8 ++++---- src/api/dashboard/login.html | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/docs/access.md b/docs/access.md index a3b8864..107de8d 100644 --- a/docs/access.md +++ b/docs/access.md @@ -102,7 +102,7 @@ curl -H "Authorization: Bearer ct1.…" http://other-node:3000/store then the user logs in again. A refreshed token may say less (`limit`), never more, and live less (`lifetime`, in seconds), and then ends its session with it: nobody refreshes it past its end. A dashboard opening another member's hands it one reading - only, for five minutes ([`dashboard.md`](dashboard.md)). + the member list only, for five minutes ([`dashboard.md`](dashboard.md)). * **Revoking**: a user disabled or removed is refused at once; all of a user's tokens, by the user (`POST /auth/logout`) or by one who may manage users (`DELETE /admin/users/{name}/tokens`), which sets `_continuum/logouts/`: tokens issued diff --git a/docs/dashboard.md b/docs/dashboard.md index 2e482e8..01507df 100644 --- a/docs/dashboard.md +++ b/docs/dashboard.md @@ -204,10 +204,11 @@ measured. * **Right-click a node**: its menu: subscribing, and opening its dashboard. * **Open a node's dashboard** (its menu): in this page, where the node publishes one (`--dashboard-url`, in the member list; e.g. behind a reverse proxy, - [`operations.md`](operations.md)). A token from this node goes along, reading only, - for five minutes: where the browser has no session at the other dashboard, its login - page begins one with it (reading only; log in there to do more), unless its proxy asks - for credentials of its own. It opens as this one was left: the map's nodes where + [`operations.md`](operations.md)). A token from this node goes along, reading the + member list only, for five minutes, enough for the map: where the browser has no + session at the other dashboard, its login page begins one with it (log in there to + see more), unless its proxy asks for credentials of its own. Whoever publishes the + dashboard (a member writes its own URL) gets that token, no more. It opens as this one was left: the map's nodes where they were (on a dynamic map they then move to their places among the other node's links, smoothly; new ones are placed among them), the view, the pins, what is in focus, the legend shown or hidden, what is open in the figures and in the document, diff --git a/src/api/dashboard/dashboard.html b/src/api/dashboard/dashboard.html index d01639e..b80e0d2 100644 --- a/src/api/dashboard/dashboard.html +++ b/src/api/dashboard/dashboard.html @@ -2078,8 +2078,9 @@ function handOff(id, handed) { legend: !document.getElementById("legends").hidden, figures: [...open], document: [...documentOpen], - // A token reading only, for five minutes: the other node's login page begins a - // session with it (`login.html`), where the browser has none there. + // A token reading the member list only, for five minutes, enough for the map: the + // other node's login page begins a session with it (`login.html`), where the + // browser has none there. Whoever publishes the dashboard gets it: no more. token: handed || undefined, })); } @@ -2451,7 +2452,6 @@ addEventListener("keydown", event => { // opened in this page; else its halo flashes red, larger than usual, and "no dashboard" // rises from it in the same red, both fading out. This node's own: here already. const FLASH_MS = 1200; -const READ_ALL = [{ path: [], rights: ["read"] }, { path: ["_continuum"], rights: ["read"] }]; async function openDashboard(id) { const member = state.graph.members[id]; if (!member || member.self) return; @@ -2460,7 +2460,7 @@ async function openDashboard(id) { if (token) { try { handed = (await tokenCall("../auth/refresh", token.token, - { limit: READ_ALL, lifetime: 300 })).token; + { limit: READ_MEMBERS, lifetime: 300 })).token; } catch (_) { /* the other dashboard asks for a login then */ } } location.assign(member.dashboard.split("#")[0] + handOff(id, handed)); diff --git a/src/api/dashboard/login.html b/src/api/dashboard/login.html index e09d31c..74c9fd1 100644 --- a/src/api/dashboard/login.html +++ b/src/api/dashboard/login.html @@ -74,7 +74,7 @@ document.getElementById("token").addEventListener("submit", async event => { try { await begin(event.target.token.value.trim()); } catch (error) { failed(error); } }); // Opened from another member's dashboard (`handOff`), sent here for want of a session: -// its token, reading only, begins one; the rest goes on to the dashboard. +// its token, reading the member list only, begins one; the rest goes on to the dashboard. (async () => { const HAND_OFF = "#continuum="; if (!location.hash.startsWith(HAND_OFF)) return; -- cgit v1.3.1