Skip to content

Move the email and push gateway reports behind admin auth #433

Closed

Description

@HMarzban

Summary

GET /api/email/health needs no authentication. It returns the mail provider name, pending and failed job counts, and sent_last_hour. GET /health/push does the same for push, plus vapid_subject. That is operational data an outsider should not see. The only consumer of both is the admin dashboard Notifications page, which needs the counts.

  • Severity: Low (information leak)
  • Area: apps/hocuspocus.server email and health routes, apps/admin-dashboard
  • Source: security review of 2026-10-06, finding L13. The push route is the same leak, found while checking this one.

Where

  • Email route: apps/hocuspocus.server/src/api/email.ts:109-117. Data: apps/hocuspocus.server/src/lib/email/service.ts:90-101.
  • Push route: apps/hocuspocus.server/src/api/routers/health.router.ts:18 → checkPushHealth (apps/hocuspocus.server/src/api/controllers/health.controller.ts:49-54). Data: apps/hocuspocus.server/src/lib/push/service.ts:62-73.
  • The dashboard calls both from the browser with no Authorization header: checkPushGatewayHealth and checkEmailGatewayHealth, apps/admin-dashboard/src/services/api.ts:435-509. It shows provider and pendingJobs (apps/admin-dashboard/src/pages/notifications.tsx:458-459, :584, :625).
  • Admin routes already pass adminAuthMiddleware and the global rate limiter: apps/hocuspocus.server/src/api/routers/admin.router.ts:31. Peers: /email/digest-grouping (:39) and /push/subscriptions (:52).
  • Public docs: apps/hocuspocus.server/API.md:35, :110-112, :1151-1153, :1278; docs/api/README.md:103; OpenAPI src/modules/openapi/domain/paths/email.ts:100-111, paths/health.ts:91 and document.ts:26.

Fix plan

  1. Add two admin routes in admin.router.ts:
    • GET /api/admin/email/gateway returns emailGateway.getHealth().
    • GET /api/admin/push/gateway returns pushGateway.getHealth().
    • Put the two handlers in apps/hocuspocus.server/src/api/controllers/admin-stats.controller.ts, beside getEmailStatsAdmin. admin.controller.ts already re-exports that file.
    • Answer 200 with the report, or 500 with { error } like their peers. The push handler answers 200 even when push is not configured, because the dashboard reads the flags from the body.
  2. Delete the public routes: emailRouter.get('/health', …) in email.ts, and health.get('/push', …) with checkPushHealth.
  3. Switch the dashboard checkEmailGatewayHealth and checkPushGatewayHealth to fetchApi('/api/admin/email/gateway') and fetchApi('/api/admin/push/gateway').
    • fetchApi adds the admin's token. Pass { signal: AbortSignal.timeout(5000) } to keep today's timeout.
    • fetchApi throws on a non-OK answer, so the existing catch returns status: 'down'. Remove the !response.ok branch.
    • Keep their return shapes, so notifications.tsx does not change.
  4. Docs: move both entries from the email and health OpenAPI paths to paths/admin.ts, and update API.md, docs/api/README.md and the push sentence in openapi/domain/document.ts:26. Add a line to apps/hocuspocus.server/CHANGELOG.md.

Layer: admin-only data goes through the admin router (apps/hocuspocus.server/CLAUDE.md §Admin API And Dashboard). Do not use requireServiceRole: the browser cannot hold the service-role key, and the admin container no longer has it (#403). Do not register adminAuthMiddleware on a /health/* path; that group skips the rate limiter, so each bad token there would reach Supabase Auth unthrottled.

Ship the server and dashboard changes in one release. Between the two container updates, the Notifications page shows the gateways as down. That gap is harmless and ends when both are live.

Out of scope

Acceptance criteria

  • GET /api/email/health and GET /health/push return 404.
  • GET /api/admin/email/gateway and GET /api/admin/push/gateway return 401 with no token and 403 for a non-admin.
  • The admin dashboard Notifications page still shows the provider, pending jobs and gateway status for both channels.
  • The OpenAPI document lists the two admin routes and not the two removed ones.

Verify

  • curl -i http://localhost:4000/api/email/health and curl -i http://localhost:4000/health/push → 404.
  • curl -i http://localhost:4000/api/admin/email/gateway → 401. With an admin access token in Authorization: Bearer … → 200.
  • cd apps/hocuspocus.server && bun test src/modules/openapi/__tests__/unit/specCoverage.test.ts.
  • Open the admin dashboard Notifications page on the local stack and check both gateway cards.

Related


Generated by Claude Code

Activity

  1. added theissue type on Oct 6, 2026
  2. changed the title [-][Security] GET /api/email/health is public and shows mail volume[/-] [+]Move the email and push gateway reports behind admin auth[/+] on Oct 6, 2026
  3. HMarzban commented on Oct 9, 2026

    @HMarzban
    CollaboratorAuthor

    Fixed and live.

    • Commits: 659029c36
    • Deployed in bbdaae66c (production run 37976785981, green).
    • Verified on production: GET /health/push answers 404. Admins read the report on GET /api/admin/push/gateway. The public GET /api/email/health returns {status, queue_connected} only.
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

    AuthSecuritySecurity, access control, and data exposurebugSomething isn't working

    Type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions