You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
No admin write action records its actor. If an admin session is stolen, or an admin makes a mistake, nobody can tell who granted admin, purged a document or deleted an account.
Severity: Low (forensics, no direct attack)
Area:apps/hocuspocus.server admin API
Source: security review of 2026-10-06 (OWASP A09)
Where
The request logger pinoLogger (apps/hocuspocus.server/src/middleware/index.ts:122-171) logs requestId, method, path, status and ip, but no user.
adminAuthMiddleware sets userId at apps/hocuspocus.server/src/api/middleware/adminAuth.ts:58 and never logs it.
document purge, privacy flag flip, anonymous cleanup, bulk stale delete
Revoking an admin deletes its admin_users row, so even created_by (packages/supabase/scripts/02-z-admin-users.sql:17) disappears.
Fix plan
In adminAuthMiddleware, after await next(), log one line for every non-GET request: actor (user id), method, route path, target id (from params), status and requestId. Use the existing adminLogger.
Better: also write an append-only admin_audit table (id, actor_id, action, target_type, target_id, status, request_id, created_at). Service role writes it. No client grant. No UPDATE or DELETE.
Show the audit list on an admin page later (optional, separate task).
Repo rules for step 2: new migration plus packages/supabase/scripts/ mirror, then bun run --filter @docs.plus/supabase_back types.
Acceptance criteria
Granting or revoking admin, purging a document, flipping privacy and deleting an account each produce one log line (or audit row) with the actor id and target id.
GET requests produce no audit rows.
A failed action (4xx or 5xx) is recorded with its status.
Status: fixed in fd96138 on claude/youthful-lovelace-3nvsc6 (fix plan step 1 only). Not merged or deployed yet.
What shipped: adminAuthMiddleware in apps/hocuspocus.server/src/api/middleware/adminAuth.ts writes one Admin action line through adminLogger after await next(). It runs for every non-GET admin request. The fields are actor, method, path, status and requestId. A 4xx or 5xx result is logged with its status.
Differences from the plan:
There is no separate target field. For single-target routes, the target id is in path: POST /api/admin/users/:id/toggle-admin, PATCH and DELETE /api/admin/documents/:id, and DELETE /api/admin/audit/ghost-accounts/:id.
Other write routes take their targets from the JSON body, so their line names no target. These are /documents/stale/bulk-delete, /audit/ghost-accounts/bulk-delete, /audit/ghost-accounts/resend-confirmation, /audit/ghost-accounts/cleanup-anonymous and /audit/notifications/disable-failed.
Step 2 (the admin_audit table) was not built. The record lasts only as long as the server logs are kept.
Step 3 was optional and was not done.
Check after deploy: do one admin write in the dashboard. Then find its Admin action line with your user id as actor.
Activity
docs: record the security and push fixes in changelogs and agent docs
[-][Security] Admin actions do not record who performed them[/-][+]Record the actor of every admin write action[/+]HMarzban commentedon Oct 6, 2026
Status: fixed in
fd96138onclaude/youthful-lovelace-3nvsc6(fix plan step 1 only). Not merged or deployed yet.What shipped:
adminAuthMiddlewareinapps/hocuspocus.server/src/api/middleware/adminAuth.tswrites oneAdmin actionline throughadminLoggerafterawait next(). It runs for every non-GET admin request. The fields areactor,method,path,statusandrequestId. A 4xx or 5xx result is logged with its status.Differences from the plan:
path:POST /api/admin/users/:id/toggle-admin,PATCHandDELETE /api/admin/documents/:id, andDELETE /api/admin/audit/ghost-accounts/:id./documents/stale/bulk-delete,/audit/ghost-accounts/bulk-delete,/audit/ghost-accounts/resend-confirmation,/audit/ghost-accounts/cleanup-anonymousand/audit/notifications/disable-failed.admin_audittable) was not built. The record lasts only as long as the server logs are kept.Check after deploy: do one admin write in the dashboard. Then find its
Admin actionline with your user id asactor.Generated by Claude Code
chore(merge): land the security and push fixes branch
refactor(hocuspocus): tidy the push, SSRF, error and admin-log follow…