Security & privacy model
Drape stores biometric data (full-body photos) for an AI pipeline. That single
fact drives most of the security design. This page is the canonical summary; the
enforcement lives in backend/server.py and the audit docs under
frontend/docs/.
Authentication
Drape uses its own MongoDB-backed JWT auth — there is no Supabase Auth, and Supabase is used for storage only.
- Library: new JWT code uses PyJWT.
python-joseis treated as deprecated because of CVE-2024-33663 / CVE-2024-33664 — no newjwt.decode()calls may be added on it. - Two dependencies, two trust levels.
get_current_user()authorizes consumer users.get_admin_user()authorizes admin-only operations — ML dataset writes, analytics admin, brand management, and thebody-references/addendpoint. Admin endpoints must never fall back toget_current_user(). - Token validated at load. The app checks
isTokenExpired()on launch; an expired token triggers logout and a redirect rather than a screen of failed requests.
Biometric data (BIPA)
A body photo is regulated biometric data. The rules below are architectural invariants, not preferences.
| Rule | Enforcement |
|---|---|
| Consent before capture | BIPA consent enforced at the backend (HTTP 403 if not consented); gates also in onboarding.tsx, fit-setup.tsx, body-photo.tsx. CONSENT_TEXT is single-sourced in src/constants/legalText.ts. |
| Photo never comes from the client on try-on | user_photo_data is not accepted in TryOnRequest; the body photo is fetched server-side from MongoDB on every request. This blocks a client from submitting a third party's photo. |
| One owner per photo | body_photo_owner_id is written on all three upload paths; a try-on is blocked with HTTP 409 if the owner does not match the authenticated user. |
| Self-healing on mismatch | GET /api/body-photo clears the fields, fires a Sentry error, and returns has_photo: false if it detects an owner mismatch. |
| Private storage | The body-photos-original, body-photos-cutout, and body-photos-mask Supabase buckets are private (verified 2026-06-03; migrations 001–003 applied). |
| Right to deletion | DELETE /api/account clears MongoDB + Supabase; surfaced as "Delete Account" in profile. |
P0 rules that must never regress
These are confirmed-and-closed vulnerabilities. Every change is checked against them:
- Body photo source — never accept
user_photo_datafrom the request body; always fetch server-side. - Try-on status ownership —
GET /api/try-on/status/{job_id}verifies the caller owns the job.user_idis stored in the Redis job payload at creation and checked at poll time (fixes the original IDOR). - Stripe webhook —
STRIPE_WEBHOOK_SECRETis required in production and hard-fails if unset. The log-and-continue branch is a dev shortcut only. - JWT library — PyJWT only for new code.
- Admin endpoints —
get_admin_user()for all ML dataset writes. - CORS —
"null"is never added to allowed origins (it would permitfile://requests). - Secrets in git history —
backend/.envwas committed once (commit64dceea).JWT_SECRET_KEYandFAL_API_KEYwere exposed and both have been rotated and validated. History still contains the old values, so repo read access must not be granted externally without a BFG history purge first.
Authorization & RLS (Layer 8)
Authentication proves identity; authorization controls access. Because all
Supabase access is server-side via the service-role key, Supabase RLS is
defense-in-depth, not the primary control. The primary control is
get_current_user() plus a user_id filter on every MongoDB query touching user
data.
auth.uid() RLS policies do not apply to Drape — service-role requests
evaluate auth.uid() as NULL. Policies use a role check
(auth.role() = 'service_role') instead. Red flags that must never appear: RLS
disabled on any table with a user_id, the service-role key in client code, or a
body-photo bucket set to public.
For the full picture see frontend/docs/LAYER_8_RLS_AUDIT.md and
AUDIT_SECURITY_PRIVACY.md.