6.2 KiB
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 requireeditoror 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_MINUTESbounded 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/mockreturns 404 unlessENVIRONMENT=development.- Razorpay webhooks: signature verified (
RAZORPAY_WEBHOOK_SECRET). - Webhook events stored with idempotency on
provider_event_id. POST …/payment/mock-completeonly whendevelopment+mockprovider.
Production startup validation (Settings.production_hardening):
PAYMENT_PROVIDER=razorpaywith Razorpay secrets set.TICKET_VERIFY_API_KEY≥ 16 characters.ADMIN_PASSWORDnot default.CORS_ORIGINSnon-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 1and Alembic migrations.
Input validation
- Request bodies use Pydantic schemas (
schemas/*). - Ticket verify:
qr_tokenlength bounds. - Order lines: quantity 1–20, max 20 lines.
File uploads
- Active
/apirouter: 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. AppErrorreturns intentionaldetailstrings 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/verifywith optionalVITE_TICKET_VERIFY_API_KEY→X-Check-In-Keyheader. - In development, if
TICKET_VERIFY_API_KEYis unset, verify is open (local convenience only).
Checklist before production
- Set strong
SECRET_KEY,TICKET_VERIFY_API_KEY, Razorpay secrets, non-defaultADMIN_PASSWORD. ENVIRONMENT=production,PAYMENT_PROVIDER=razorpay.- Tight
CORS_ORIGINSto real site origin(s). - TLS everywhere; HSTS at edge.
- Do not mount legacy
app/routers/*without security review. - Enable edge rate limiting and monitoring on auth/webhook paths.
Related tests
tests/test_security.py— JWT types, bcrypttests/test_security_hardening.py— production settings, mock webhooktests/test_payments.py— verify + webhook idempotencytests/test_digital_tickets.py— QR reusetests/test_admin_api.py— attendee blocked from admin