From b3f282adab57c39232706ea48c0dfd98573d4061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sat, 9 May 2026 17:41:46 +0200 Subject: [PATCH] fix(security): block force-logout from targeting the admin's own session The UI already disabled the per-row force-logout button for the active admin (user-management-section.tsx:295), but the route handler had no matching server-side check. A direct curl/DevTools call (or a compromised admin session) could revoke the caller's own sessions and bounce them to /auth/login mid-action. Mirrors the UI gate with a 422 + an `admin.user.force-logout.denied` audit entry tagged `reason: self_target`. Admin-on-admin force-logout remains permitted (today's intentional behaviour). Closes H1 from .planning/phase-6-code-review-findings.md. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/app/api/admin/users/[id]/force-logout/route.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/app/api/admin/users/[id]/force-logout/route.ts b/src/app/api/admin/users/[id]/force-logout/route.ts index f2d646c1a..be482c7b2 100644 --- a/src/app/api/admin/users/[id]/force-logout/route.ts +++ b/src/app/api/admin/users/[id]/force-logout/route.ts @@ -31,6 +31,19 @@ export const POST = apiHandler( }, }); + // Mirror the UI gate at user-management-section.tsx:295 — admins + // mid-action shouldn't bounce themselves to /auth/login. A + // determined admin can revoke another admin (intentional today), + // but the self-target case is always a footgun. + if (admin.id === id) { + await auditLog("admin.user.force-logout.denied", { + userId: admin.id, + ipAddress: getClientIp(request), + details: { targetUserId: id, reason: "self_target" }, + }); + return apiError("Cannot force-logout your own session", 422); + } + const target = await prisma.user.findUnique({ where: { id }, select: { id: true, username: true },