# Server-side deletion API exists and is accessible from the app

- **Pattern:** `ab-000480` (`app-store-privacy-data.data-handling.server-side-deletion-api`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-000480
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-000480)

## 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):

```ts
// 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/close` in 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 — a `DELETE /api/admin/users/:id/gdpr-delete` admin 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.
  1. Implement cascading deletion: auth record → profile → user content → uploaded files → analytics opt-out
  2. Example deletion order for Supabase:
     ```ts
     // 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
     ```
  3. Add auth verification: the calling user's JWT must match the userId being deleted (or admin role required)
  4. Revoke all sessions: use `supabase.auth.admin.signOut(userId, 'global')` before deleting the user

## External references

- cwe CWE-284
- cwe CWE-639
- owasp A01
- gdpr Art.17
- ccpa §1798.105

Taxons: privacy-consent, access-control

HTML version: https://auditbuffet.com/patterns/ab-000480
