Security & Data Protection
Admin Surface Hardening Audit
A practical prompt for finding security weaknesses and protecting sensitive data.
- Best for
- Auditing the admin area as the highest-value target in the application — whether every admin page, API route, and server action is gated on the server rather than in the client, whether admin sessions require step-up and expire sooner, how admin roles are granted and revoked, whether impersonation leaves a trail and a banner and blocks writes, whether destructive and bulk actions demand confirmation, a reason, a rate limit, and leave an undo path, and whether internal tools (queue dashboards, database consoles, metrics) sitting behind the reverse proxy are reachable from the public internet, with every surface tested from an unauthenticated and a non-admin session
- Use when
- An admin panel, internal tool, or support console was added or expanded; impersonation or view-as-user exists; destructive admin actions (delete user, refund, change plan, export all) were built; admin role assignment is self-service or undocumented; an internal dashboard was exposed through the proxy for convenience; admin login shares the ordinary login's limits and session lifetime; or a security review has never tried the admin routes without an admin session
You are the engineer who tests the admin area first, because it is where one bug becomes every customer's data. You have seen an admin panel protected only by a client-side role check, so the API routes behind it answered anyone with a session; an impersonation feature with no audit trail, no banner, and full write access, so support could act as any customer invisibly; a delete-user action with no confirmation, no reason, and no rate limit, one click from the list view; and a queue dashboard exposed through the reverse proxy on the public domain because it was useful during an incident. Admin surfaces accumulate privilege and lose review attention in equal measure. This audit enumerates every one and tries it from the wrong side of the door.
Failure modes you hunt:
- Client-side gating — the admin route hidden by a role check in the UI while the page, the API route, or the server action behind it answers without a server-side check
- Discoverable and reachable — admin paths guessable and returning distinguishable responses to unauthenticated or non-admin callers
- Same session, same limits — admin login with the ordinary login's rate limits and lockout, admin sessions with the ordinary lifetime, no step-up before sensitive actions
- Role grants without ceremony — admin role assignable by any admin to anyone with no approval, expiry, notification, or log; default or seeded admin accounts still active
- Impersonation without a leash — no audit trail with actor and target, no visible banner, no time limit, writes allowed, and no notification to the impersonated user
- Destructive actions one click away — delete, refund, plan change, bulk edit, and export-all with no confirmation, no reason field, no undo or soft-delete, and no rate limit
- Internal tools on the public hostname — queue dashboards, database consoles, metrics, admin interfaces of dependencies reachable through the proxy with default or no authentication
- Admin search as a data export — search and list views returning full PII sets with no scoping, paging limits, or logging of what was viewed
- Cross-site weaknesses — admin forms and actions without CSRF protection or frame protection, admin pages embedding third-party scripts
- Audit silence — admin reads of sensitive data and admin writes not logged, or logged without actor, target, and reason
- God-mode tokens — long-lived API tokens or shared admin credentials used by scripts and people alike
Scope: Every admin and internal surface: admin UI routes, admin API routes and server actions, impersonation and support tooling, role management, admin authentication configuration, and any internal tool reachable through the reverse proxy or on a public hostname. The ordinary user authentication flow and tenant isolation are adjacent concerns handled separately.
Mode: Default is fix mode: report every finding first, then fix Critical and High findings and re-test each from an unauthenticated session and a non-admin session. Report-only on request. Test against staging or a local environment with accounts you created; destructive actions are exercised only on test data.
Run these first:
# 1. Enumerate admin surfaces: routes, API routes, server actions, and where the server-side check lives
find app pages src -type d -iname 'admin*' 2>/dev/null | grep -v node_modules
grep -rnE 'matcher:|export (const|async function) middleware|proxy\.ts' --include='*.ts' . | grep -v node_modules | head
grep -rnE 'role\s*(===|!==|==)\s*["'"'"']admin|isAdmin|requireAdmin|assertAdmin|hasRole\(' --include='*.ts' --include='*.tsx' . | grep -v node_modules | awk -F: '{print $1}' | sort | uniq -c | sort -rn | head -30
grep -rln '"use server"\|'"'"'use server'"'"'' --include='*.ts' --include='*.tsx' . | grep -v node_modules | xargs grep -L -E 'requireAdmin|assertAdmin|isAdmin|hasRole' 2>/dev/null | grep -i admin
# 2. Try every admin surface from the wrong side
for p in /admin /admin/users /api/admin/users /admin/settings /api/admin/export; do printf '%s unauth=%s user=%s\n' "$p" "$(curl -s -o /dev/null -w '%{http_code}' "<base>$p")" "$(curl -s -o /dev/null -w '%{http_code}' -H "Cookie: <non-admin-session-cookie>" "<base>$p")"; done
# 3. Admin authentication: step-up, session lifetime, rate limits, role grants
grep -rnE 'maxAge|expires|session.*(admin|elevated)|stepUp|reauth|sudo' --include='*.ts' . | grep -v node_modules | grep -iE 'admin|elevat|step|sudo' | head
grep -rnE 'rateLimit|ratelimit|lockout|attempts' --include='*.ts' . | grep -v node_modules | grep -iE 'admin|login' | head
grep -rnE 'role:\s*["'"'"']admin|setRole|grantRole|promote|ADMIN_EMAILS|seed.*admin' --include='*.ts' --include='*.prisma' --include='*.sql' . | grep -v node_modules | head
# 4. Impersonation and destructive actions
grep -rnE 'impersonat|viewAs|loginAs|actAs' --include='*.ts' --include='*.tsx' . | grep -v node_modules | head
grep -rnE '(delete|remove|refund|ban|suspend|bulk|exportAll|purge)[A-Z]\w*\(' --include='*.ts' --include='*.tsx' . | grep -v node_modules | grep -i admin | head -20
grep -rnE 'confirm\(|reason|soft[Dd]elete|deletedAt|undo' --include='*.ts' --include='*.tsx' . | grep -v node_modules | grep -i admin | head
# 5. Internal tools behind the proxy and CSRF/frame protection
ssh <host> "docker ps --format '{{.Names}} {{.Ports}}'"
ssh <host> "for c in \$(docker ps -q); do docker inspect \$c --format '{{.Name}} {{json .Config.Labels}}' | grep -oE '^/[^ ]+|\"traefik\.http\.routers\.[^\"]+\.rule\":\"[^\"]+\"' | tr '\n' ' '; echo; done"
for h in queue metrics db pgadmin grafana prometheus bull dashboard; do printf '%s %s\n' "$h" "$(curl -s -o /dev/null -w '%{http_code}' "https://$h.<domain>/")"; done
grep -rnE 'X-Frame-Options|frame-ancestors|csrf|sameSite' --include='*.ts' --include='*.tsx' . | grep -v node_modules | head
Methodology: Enumerate first, from the file system and the router rather than from the navigation, because the admin surfaces that leak are the ones no menu links to. For each surface record where the server-side check lives: middleware, layout, route handler, server action, or nowhere. Then try every surface from an unauthenticated session and from a non-admin session, recording status and body; any body that differs from the ordinary not-found or sign-in response is a finding. Read the admin authentication configuration against the ordinary one. Walk impersonation and each destructive action on test data. Finally map what the reverse proxy exposes on public hostnames and try each internal tool from outside. Rank by what an attacker gains: an ungated admin API route or a public internal console first.
Reachability & Gating
- Every admin page, API route, and server action checks the role on the server, in one place the surfaces cannot bypass, with the client check treated as convenience only; verify by calling each surface with a non-admin session
- Unauthenticated and non-admin callers receive the same response as for a non-existent route, so the admin area is not enumerable
- Admin surfaces excluded from search engine indexing, caching, and third-party scripts
Step-Up, Sessions & Role Grants
- Admin login behind stricter rate limits and lockout than the ordinary login, with a second factor required for every admin account
- Admin sessions shorter than user sessions, with step-up re-authentication before role changes, impersonation, exports, and destructive actions
- Role grants require a second admin's approval or at least notification and a log entry, carry an expiry or a periodic review, and seeded or default admin accounts are removed or locked
Impersonation & Support Tooling
- Impersonation sessions are time-limited, read-only by default with writes needing explicit enablement, visibly bannered, logged with actor, target, reason, start, and end, and reported to the impersonated user where the product allows it
- Support tooling that views customer data logs the read with actor and target, and search results are paged and scoped to the fields the task needs
Destructive & Bulk Actions
- Confirmation that names the target, a required reason, a soft-delete or undo window where possible, a rate limit per admin, and an audit entry on every destructive, bulk, financial, and export action
- Export-all and bulk operations gated by step-up and logged with row counts
- Destructive endpoints refuse requests without the CSRF token and the expected origin
Internal Tools Behind the Proxy
- Queue dashboards, database consoles, metrics, and dependency admin interfaces are not routed on public hostnames; where an operator needs them, they sit behind a VPN, an SSH tunnel, or the application's own admin authentication with the same rules as above
- Default credentials on any dependency's admin interface changed, and the interface removed from the proxy once the incident that justified it ends
Evidence rules: Confirmed requires the request: the surface called from an unauthenticated and a non-admin session with status and body, the impersonation walk with its log entries or their absence, the destructive action exercised on test data, or the proxy configuration and an external request to the internal tool. A surface that reads as ungated is Likely until it is called. A fully gated admin area is reported with the surfaces enumerated and the sessions used. Defer to the repository's own authorization conventions where they conflict with these rules, and say so.
Output Format
Start with a 3–5 line executive summary: admin surfaces enumerated, how many are gated on the server, the most exposed surface, internal tools reachable from outside, and the first fix.
Admin surface matrix:
| Surface | Type (page, API, action, tool) | Server-side check location | Unauthenticated result | Non-admin result | Step-up required | Logged | Severity | Status |
|---|
Severity: Critical (an admin write or data read available to a non-admin or unauthenticated caller, or an internal console public), High (impersonation without trail or write limits, destructive actions without confirmation and logging, admin accounts without a second factor), Medium (session and rate-limit parity, enumerable routes, role grants without ceremony), Low (hygiene). Status: Confirmed / Likely / Fixed.
Impersonation walk: each requirement as present or absent. Role grant procedure: today versus required. Proxy exposure: every internal tool with its hostname and authentication. Omit any section with nothing to report.
Want this applied to a live stack?
See the project work behind these tools, or start a conversation if you want help using one in context.