mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-08 16:39:42 +02:00
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:
@@ -35,6 +35,7 @@ import {
|
||||
UserStoreError,
|
||||
} from '../../user-store.js';
|
||||
import { getAuthUser, requireAdmin, revokeUserSessions } from '../route-helpers.js';
|
||||
import { webviewCapabilities } from '../../webview-capabilities.js';
|
||||
import { appendAdminAudit } from '../admin-audit.js';
|
||||
import { SseEvent } from '../sse-events.js';
|
||||
import type { AuthPort } from '../ports/auth-port.js';
|
||||
@@ -179,7 +180,10 @@ export function registerAdminRoutes(app: FastifyInstance, ctx: SessionPort & Aut
|
||||
if (!gate(req, reply)) return;
|
||||
const { username } = req.params as { username: string };
|
||||
const revoked = revokeUserSessions(ctx.authSessions, username);
|
||||
audit(req, 'user.logout', username, { revoked });
|
||||
// Web-tab proxy capabilities are a second credential the cookie purge does not
|
||||
// touch; a forced logout that left them alive would not be a logout.
|
||||
const revokedWebviews = webviewCapabilities.revokeOwner(normalizeUsername(username));
|
||||
audit(req, 'user.logout', username, { revoked, revokedWebviews });
|
||||
return { success: true, data: { revoked } };
|
||||
});
|
||||
|
||||
@@ -201,6 +205,7 @@ export function registerAdminRoutes(app: FastifyInstance, ctx: SessionPort & Aut
|
||||
await ctx.cleanupSession(id, true, 'admin_delete_user').catch(() => {});
|
||||
}
|
||||
revokeUserSessions(ctx.authSessions, username);
|
||||
webviewCapabilities.revokeOwner(normalizeUsername(username));
|
||||
if (deleteSpace) await deleteUserSpace(username);
|
||||
audit(req, 'user.delete', username, { deleteSpace, killedSessions: owned.length });
|
||||
ctx.broadcast(SseEvent.AdminUsersChanged, {});
|
||||
|
||||
@@ -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 {};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user