194 lines
6.2 KiB
Markdown
194 lines
6.2 KiB
Markdown
# 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
|