Server-side deletion API exists and is accessible from the app
Why it matters
An in-app 'Delete Account' button that calls a stub function or only deletes the auth record — leaving profile tables, user content, and uploaded files intact — creates GDPR Art.17 and CCPA §1798.105 liability while giving users false confidence their data is gone. The more dangerous failure: an unauthenticated deletion endpoint (OWASP A01, CWE-284, CWE-639) allows any caller to delete any user's account by guessing or enumerating user IDs — a complete access-control failure that turns a privacy feature into a weaponisable IDOR vulnerability.
Severity rationale
Medium because an incomplete server-side deletion endpoint creates regulatory liability and, if unauthenticated, becomes an IDOR vulnerability enabling account destruction at scale.
Remediation
Implement cascading deletion: auth record → profile → user content → uploaded files → analytics opt-out. Verify the calling JWT matches the userId being deleted (or require admin role):
// Edge Function: delete-account.ts
// Verify calling user matches target userId
await supabase.from('user_files').delete().eq('user_id', userId);
await supabase.from('posts').delete().eq('user_id', userId);
await supabase.from('profiles').delete().eq('id', userId);
await supabase.auth.admin.signOut(userId, 'global');
await supabase.auth.admin.deleteUser(userId);
Search for the deletion endpoint in src/app/api/ or route handlers and verify it has an authentication check before the deletion logic. A no-op stub or missing auth check both fail this check regardless of whether the in-app UI exists.
Detection
- ID:
server-side-deletion-api - Severity:
medium - What to look for: Count all relevant instances and enumerate each. Even if the in-app deletion UI exists (checked separately), verify the server-side endpoint that processes the deletion. Search for:
DELETE /api/user,DELETE /api/account,DELETE /api/v1/users/:id,/api/account/delete,POST /api/account/closein route handlers (Next.js API routes, Express routes, Supabase Edge Functions, FastAPI routes, etc.). Verify the handler: (a) authenticates the request (checks session/token — a user should only be able to delete their own account); (b) actually deletes or schedules deletion of user data (not just the auth record — also profile data, posts, messages, uploaded files); (c) revokes all sessions after deletion; (d) returns an appropriate success response. Also look for a privacy request handling mechanism (separate from self-service deletion) for GDPR Article 17 requests received by email — aDELETE /api/admin/users/:id/gdpr-deleteadmin route or similar. - Pass criteria: A server-side deletion endpoint exists, is authenticated, and deletes associated user data beyond just the auth record. At least 1 implementation must be verified. Sessions are revoked after deletion.
- Fail criteria: No server-side deletion endpoint found; endpoint exists but only deletes the auth record without touching profile/content data; endpoint is unauthenticated (IDOR vulnerability); endpoint is a no-op stub.
- Skip (N/A) when: App has no user accounts; or app is purely client-side with no server-side user data storage.
- Detail on fail:
"DELETE /api/user endpoint found but only calls supabase.auth.admin.deleteUser() — profile table, user_posts, and uploaded files are not deleted"or"Account deletion endpoint found but has no authentication check — any caller can delete any user's account (IDOR)" - Remediation: Incomplete server-side deletion creates GDPR/CCPA liability even if the in-app UI works correctly.
- Implement cascading deletion: auth record → profile → user content → uploaded files → analytics opt-out
- Example deletion order for Supabase:
// Edge Function: delete-account.ts await supabase.from('user_files').delete().eq('user_id', userId); await supabase.from('posts').delete().eq('user_id', userId); await supabase.from('profiles').delete().eq('id', userId); await supabase.auth.admin.deleteUser(userId); // storage.remove() for uploaded files - Add auth verification: the calling user's JWT must match the userId being deleted (or admin role required)
- Revoke all sessions: use
supabase.auth.admin.signOut(userId, 'global')before deleting the user
External references
- cwe · CWE-284 — Improper Access Control
- cwe · CWE-639 — Authorization Bypass Through User-Controlled Key (IDOR)
- owasp:2021 · A01 — Broken Access Control
- gdpr · Art.17 — Right to erasure ('right to be forgotten')
- ccpa · §1798.105 — Right to deletion of personal information
Taxons
History
- 2026-04-18·v1.0.0·Initial import from app-store-privacy-data·automated