fix(webview): revoke proxy capabilities on logout and stamp Referrer-Policy

WebviewCapabilityStore.revokeOwner() shipped for two releases with a docstring
claiming logout called it and no caller at all. The capability is a bearer
credential exempt from cookie auth with a rolling TTL refreshed on every use, so
a proxy URL that leaked (browser history, a screenshot, a dashboard with a loose
referrer policy) stayed valid for as long as anything kept polling it.

- POST /api/logout revokes the caller's capabilities (all of them in single-user
  mode), the admin forced logout revokes the target user's, and user deletion
  revokes whatever that user had open. revokeOwner returns the count for the
  admin audit line.
- Proxied responses carry `Referrer-Policy: same-origin` and the upstream's own
  policy is dropped: every URL inside the frame carries the capability, and a
  dashboard on no-referrer-when-downgrade or unsafe-url handed it to any
  third-party host it linked. Verified with Playwright that a sandboxed frame
  under an upstream `unsafe-url` sends no Referer to a third party while the
  root-absolute fetch and the CSS-triggered 404 fallback still reach the
  dashboard.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WKtW48T1UjAaecHAJxKobE
This commit is contained in:
Codeman maintainer
2026-09-04 15:21:13 +02:00
parent 550e08a791
commit 2ab21c1b32
6 changed files with 209 additions and 4 deletions
+5
View File
@@ -31,6 +31,7 @@ import {
} from '../../types.js';
import { Session, isAltScreenStripMode, isExternalCliMode, isMuxAltScreenOnlyStripMode } from '../../session.js';
import { SseEvent } from '../sse-events.js';
import { webviewCapabilities } from '../../webview-capabilities.js';
import {
CreateSessionSchema,
SessionNameSchema,
@@ -821,6 +822,10 @@ export function registerSessionRoutes(
if (sessionToken) {
ctx.authSessions?.delete(sessionToken);
}
// The web-tab proxy authenticates on capabilities, not on this cookie, so a
// logout has to retire them too or every dashboard URL opened during this
// login keeps relaying without one (WebviewCapabilityStore.revokeOwner).
webviewCapabilities.revokeOwner(ownerFor(req));
reply.clearCookie(AUTH_COOKIE_NAME, { path: '/' });
return {};
});