Skip to content

Security

If you discover a security vulnerability, please email ernest@clinicflow.lat with:

  • Description of the vulnerability
  • Steps to reproduce
  • Potential impact
  • Suggested fix (if any)

Do not open a public GitHub issue for security vulnerabilities.

Response time: within 48 hours. We take security reports seriously and will work with you to understand and address the issue.


Multi-Tenant Isolation (Critical — Read Before Adding Queries)

Section titled “Multi-Tenant Isolation (Critical — Read Before Adding Queries)”

Protected requests use a publishable-key Supabase client carrying the original user bearer token. Jobs and trusted tenant work use a five-minute tenant-system token. Both are enforced by PostgreSQL RLS.

Every query against a clinic-owned table must still be explicitly scoped:

.eq('clinic_id', req.user.clinicId)
// For the clinics table itself:
.eq('id', req.user.clinicId)

req.user.clinicId comes from the verified ES256 JWT, never from request input. RLS independently validates the signed clinic claim, current user role, activation state, and related parent rows.

src/__tests__/tenantScopingGuard.test.js scans clinic-scoped routers and fails if a clinic-owned table lacks an explicit scope or reviewed scope-by-parent exception. src/__tests__/privilegedImportGuard.test.js limits service-role imports to the audited global/discovery modules.

Route Auth Model
routes/superadmin.js X-Admin-Key header (cross-clinic intentionally)
services/calendarFeedDiscovery.js Public ICS lookup by unguessable per-doctor token
Scope-by-parent e.g. patients.js fetches appointments by patient_id after verifying patient belongs to clinic

  • Passwords: bcrypt with cost factor 12
  • JWT: ES256, 8-hour expiry, fixed role=authenticated, plus sub, clinic_id, and app_role
  • Session freshness: RLS checks the live user, clinic, and stored role on every database statement
  • Admin operations: HTTP middleware and database policies both require the admin application role
  • Superadmin: Separate X-Admin-Key auth using crypto.timingSafeEqual (not JWT)
  • Patient DUI numbers: AES-256 (ENCRYPTION_KEY)
  • WhatsApp access tokens: AES-256 (ENCRYPTION_KEY)
  • API responses expose only a configured/not-configured mask; encrypted tokens are fetched only with tenant-system access
  • Meta/WhatsApp: HMAC-SHA256 signature verification on all inbound webhooks
  • LemonSqueezy: Signature verification using LEMONSQUEEZY_SIGNING_SECRET
  • Both use timing-safe comparison
  • Auth endpoints: 10 requests / 15 min / IP (failed attempts only)
  • Demo form: 5 requests / hour / IP
  • ICS feeds: 30 requests / 15 min / IP
  • Global: 100 requests / 15 min / IP
  • CORS: Locked to FRONTEND_URL env var
  • Helmet: Security headers on all responses (HSTS, X-Content-Type-Options, etc.)
  • Audit logs: Admin actions logged to audit_logs table with IP address
  • No secrets in responses: Temporary passwords returned once, tokens masked
  • Phone numbers: Validated to E.164 format before WhatsApp interactions