# Security — Desi Clans **Last reviewed:** 2026-04-04 This document describes security assumptions, controls, and operational requirements for the Desi Clans monorepo (FastAPI API + React SPA). --- ## Threat model (summary) | Asset | Primary risks | |-------|----------------| | User accounts | Credential stuffing, session theft | | Admin panel | Privilege abuse, catalog tampering | | Orders / payments | Price tampering, replay, webhook forgery | | Digital tickets | QR leakage, double entry, scanner abuse | | Media uploads | Malware, path traversal (legacy paths only) | **Out of scope for v1:** DDoS at scale (use CDN/WAF in production), PCI DSS Level 1 (Razorpay.js handles card data). --- ## Authentication & JWT ### Attendees (`/api/auth/*`) - Passwords hashed with **bcrypt** (passlib). Plain passwords never stored or returned. - JWT **HS256**, claim `type: "user"`, `sub` = numeric user id. - Register/login: Pydantic validation (email normalized, password **≥ 8** characters). - **Rate limit:** 20 login / 10 register requests per IP per minute (in-process). ### Admins (`/api/admin/auth/*`) - Separate JWT claim `type: "admin"`; attendee tokens **cannot** call admin routes (tested). - Roles: `editor`, `super_admin` — all admin API routes currently require `editor` or above. - **Rate limit:** 20 login attempts per IP per minute. ### Frontend token storage - Attendee token: `localStorage` (`desiclans_access_token`). - Admin token: separate key (`desiclans_admin_token`). - **Assumption:** XSS on the marketing or admin origin equals token compromise. Mitigate with CSP, dependency hygiene, and short TTL if needed. ### Production configuration - `SECRET_KEY` ≥ 32 chars, not on blocklist (`config.py`). - `ACCESS_TOKEN_EXPIRE_MINUTES` bounded 5–20160. --- ## Authorization | Endpoint class | Enforcement | |----------------|-------------| | Public catalog | No auth | | Orders, tickets (owner) | `CurrentUser` + `user_id` match on orders/tickets | | Gate verify | `X-Check-In-Key` (+ rate limit) | | Admin CRUD | `EditorAdmin` JWT | | Admin ticket verify | **Editor JWT + same gate key** as public verify | **QR reuse:** `verify_ticket_at_gate` locks ticket row (`SELECT … FOR UPDATE`), sets `used` once; second scan returns `valid: false` (HTTP 200). **Order manipulation:** Clients send `event_id` + line items only; **prices and totals** come from `TicketType` rows under row lock. Ticket types must belong to the event. **Payment verify:** User must own order; `provider_order_id` must match order; signature verified via Razorpay/mock HMAC; amount/currency compared to `order.total_paise`. --- ## Payments & webhooks | Provider | Use | |----------|-----| | `mock` | Local dev / CI only | | `razorpay` | Production | - **`POST /api/webhooks/mock`** returns **404** unless `ENVIRONMENT=development`. - Razorpay webhooks: signature verified (`RAZORPAY_WEBHOOK_SECRET`). - Webhook events stored with **idempotency** on `provider_event_id`. - `POST …/payment/mock-complete` only when `development` + `mock` provider. **Production startup validation** (`Settings.production_hardening`): - `PAYMENT_PROVIDER=razorpay` with Razorpay secrets set. - `TICKET_VERIFY_API_KEY` ≥ 16 characters. - `ADMIN_PASSWORD` not default. - `CORS_ORIGINS` non-empty. --- ## CORS - Explicit origin list from `CORS_ORIGINS` (no `*`). - `allow_credentials=True` — **do not** add untrusted origins in production. --- ## SQL injection - Business logic uses **SQLAlchemy ORM** / parameterized queries. - Raw SQL limited to health check `SELECT 1` and Alembic migrations. --- ## Input validation - Request bodies use **Pydantic** schemas (`schemas/*`). - Ticket verify: `qr_token` length bounds. - Order lines: quantity 1–20, max 20 lines. --- ## File uploads - **Active `/api` router:** no upload endpoints. - Legacy `app/routers/admin.py` (not mounted): extension whitelist, 8MB cap, PIL re-encode to WebP. - **Assumption:** Keep legacy routers unmounted or remove before exposing uploads on production API. --- ## Secrets & environment | Variable | Notes | |----------|--------| | `SECRET_KEY` | JWT signing; rotate with forced re-login | | `PAYMENT_MOCK_SECRET` | Dev only | | `RAZORPAY_*` | Production payments | | `TICKET_VERIFY_API_KEY` | Gate scanners; constant-time compare | | `DATABASE_URL` | Never commit; least-privilege DB user | See `backend/.env.example` and `frontend/.env.example`. --- ## API error leakage - Production: `debug=False`, OpenAPI disabled, generic 500 body. - `AppError` returns intentional `detail` strings only. - Development may expose exception text on 500. --- ## Rate limiting In-process sliding window (`app/core/rate_limit.py`) on: - Auth login/register - Admin login - Ticket verify - Payment verify **Limitation:** Per-process only; use reverse-proxy or WAF limits for multi-instance deployments. --- ## QR codes - Opaque `qr_token` (url-safe random); payload is JSON `{"v":1,"token":…}` without embedded HMAC. - Security relies on **HTTPS**, token secrecy, and server-side verify. - Ticket detail (`GET /api/tickets/{id}`) owner-only; includes QR image for valid paid orders. --- ## Frontend (checkout) - Razorpay **key id** only in browser; secrets stay on server. - No payment card data touches Desi Clans backend. --- ## Scanner / admin verify UI - Staff UI calls `POST /api/tickets/verify` with optional `VITE_TICKET_VERIFY_API_KEY` → `X-Check-In-Key` header. - In development, if `TICKET_VERIFY_API_KEY` is unset, verify is open (local convenience only). --- ## Checklist before production 1. Set strong `SECRET_KEY`, `TICKET_VERIFY_API_KEY`, Razorpay secrets, non-default `ADMIN_PASSWORD`. 2. `ENVIRONMENT=production`, `PAYMENT_PROVIDER=razorpay`. 3. Tight `CORS_ORIGINS` to real site origin(s). 4. TLS everywhere; HSTS at edge. 5. Do not mount legacy `app/routers/*` without security review. 6. Enable edge rate limiting and monitoring on auth/webhook paths. --- ## Related tests - `tests/test_security.py` — JWT types, bcrypt - `tests/test_security_hardening.py` — production settings, mock webhook - `tests/test_payments.py` — verify + webhook idempotency - `tests/test_digital_tickets.py` — QR reuse - `tests/test_admin_api.py` — attendee blocked from admin