Skip to content

Record the actor of every admin write action #404

Description

@HMarzban

Summary

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.
  • Success paths log nothing:
    • toggleAdminRole — apps/hocuspocus.server/src/api/services/adminStats.service.ts:393-420
    • deleteGhostAccount — apps/hocuspocus.server/src/api/services/adminGhostAccounts.service.ts:276-315
    • 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

  1. 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.
  2. 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.
  3. 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.

Generated by Claude Code

Activity

  1. added theissue type on Oct 6, 2026
  2. changed the title [-][Security] Admin actions do not record who performed them[/-] [+]Record the actor of every admin write action[/+] on Oct 6, 2026
  3. added
    enhancementNew feature or request
    SecuritySecurity, access control, and data exposure
    and removed
    bugSomething isn't working
    on Oct 6, 2026
  4. HMarzban commented on Oct 6, 2026

    @HMarzban
    CollaboratorAuthor

    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.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    DevOpsSecuritySecurity, access control, and data exposureenhancementNew feature or request

    Type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions