Skip to content

Security Audit

Last updated: 2026-03-04


DomainCriticalHighMediumLow
Authentication & Sessions2 ✅4 ✅44
Input Validation & Injection1 ✅030
Payments & Billing03 ✅40
Authorization & IDOR3 ✅3 ✅12
Infrastructure & Config04 ✅41
GDPR & Privacy03 ✅53
Total61214 (13 fixed, 1 won’t fix)8

Overall posture: All critical, high, and medium severity issues have been remediated. The codebase now has OAuth CSRF protection, multi-tenant authorization, secure env handling, security headers, magic link hashing, PII redaction, GDPR-compliant erasure, and HTTPS enforcement.


CRIT-1: SQL Injection via sql.raw() String Concatenation — ✅ FIXED

Section titled “CRIT-1: SQL Injection via sql.raw() String Concatenation — ✅ FIXED”
Fileapps/api/src/services/teacher/retention-metrics-service.ts
TypeSQL Injection
CVSS9.8
Status✅ Fixed — replaced sql.raw() with Drizzle’s inArray() operator

CRIT-2: Missing OAuth 2.0 State Parameter (CSRF) — ✅ FIXED

Section titled “CRIT-2: Missing OAuth 2.0 State Parameter (CSRF) — ✅ FIXED”
Fileapps/api/src/services/auth/google-oauth.ts, apps/api/src/routes/auth/index.ts
TypeCSRF / Account Takeover
Status✅ Fixed — crypto.randomBytes(32) state stored in Redis (10-min TTL), validated and deleted on callback

CRIT-3: IDOR — Enrollment Endpoints Missing teacherId Check — ✅ FIXED

Section titled “CRIT-3: IDOR — Enrollment Endpoints Missing teacherId Check — ✅ FIXED”
Fileapps/api/src/services/billing/enrollment-service.ts
RoutesGET /teacher/enrollments/:id, POST /teacher/enrollments/:id/cancel
Status✅ Fixed — teacherId parameter added to getEnrollmentById() and cancelEnrollment(), all callers updated

CRIT-4: IDOR — Session Participants Missing teacherId Check — ✅ FIXED

Section titled “CRIT-4: IDOR — Session Participants Missing teacherId Check — ✅ FIXED”
Fileapps/api/src/services/scheduling/session-participant-service.ts
RoutesGET/POST/DELETE /teacher/sessions/:id/participants
Status✅ Fixed — verifySessionOwnership() helper added, teacherId parameter in all public methods, all callers updated

CRIT-5: IDOR — Service Schedule Items Missing teacherId Check — ✅ FIXED

Section titled “CRIT-5: IDOR — Service Schedule Items Missing teacherId Check — ✅ FIXED”
Fileapps/api/src/services/scheduling/service-schedule-service.ts
RouteGET /teacher/services/:id/schedule
Status✅ Fixed — listScheduleItems() now requires teacherId and verifies service ownership

CRIT-6: Cross-Tenant Enrollment — ✅ FIXED

Section titled “CRIT-6: Cross-Tenant Enrollment — ✅ FIXED”
Fileapps/api/src/services/billing/enrollment-service.ts
TypeBroken Access Control
Status✅ Fixed — student.teacherId !== service.teacherId check added before enrollment creation

HIGH-1: Hardcoded Session Secret Fallback — ✅ FIXED

Section titled “HIGH-1: Hardcoded Session Secret Fallback — ✅ FIXED”
Fileapps/api/src/lib/env.ts
Status✅ Fixed — requireEnv() helper crashes on startup if SESSION_SECRET is missing in production. Dev fallback only in non-production.

HIGH-2: Stripe Webhook Secret Empty Fallback — ✅ FIXED

Section titled “HIGH-2: Stripe Webhook Secret Empty Fallback — ✅ FIXED”
Fileapps/api/src/services/billing/stripe-service.ts, apps/api/src/lib/env.ts
Status✅ Fixed — STRIPE_WEBHOOK_SECRET required via requireEnv(), crashes on missing in production. Uses env.STRIPE_WEBHOOK_SECRET instead of process.env.

HIGH-3: Webhook Idempotency Race Condition (TOCTOU) — ✅ FIXED

Section titled “HIGH-3: Webhook Idempotency Race Condition (TOCTOU) — ✅ FIXED”
Fileapps/api/src/routes/webhooks/index.ts
Status✅ Fixed — replaced SELECT-then-INSERT with atomic INSERT ... ON CONFLICT DO NOTHING, checks returning() for dedup

HIGH-4: Webhook Marked Processed Before Handler Succeeds — ✅ FIXED

Section titled “HIGH-4: Webhook Marked Processed Before Handler Succeeds — ✅ FIXED”
Fileapps/api/src/routes/webhooks/index.ts
Status✅ Fixed — on handler failure, webhook record is deleted and 500 is returned so Stripe retries

HIGH-5: Dev Login Accessible Without Strong Controls — ✅ FIXED

Section titled “HIGH-5: Dev Login Accessible Without Strong Controls — ✅ FIXED”
Fileapps/api/src/routes/auth/index.ts
Status✅ Fixed — rate limit (5/hour) and audit logging added to dev-login route

HIGH-6: No Session Invalidation on Account Changes — ✅ FIXED

Section titled “HIGH-6: No Session Invalidation on Account Changes — ✅ FIXED”
Fileapps/api/src/services/auth/session-service.ts
Status✅ Fixed — invalidateAllSessionsExcept(teacherId, currentSessionId) method added

HIGH-7: Missing Security Headers — ✅ FIXED

Section titled “HIGH-7: Missing Security Headers — ✅ FIXED”
Fileapps/api/src/app.ts
Status✅ Fixed — @fastify/helmet installed with HSTS (production), X-Frame-Options, X-Content-Type-Options, Referrer-Policy

HIGH-8: PostgreSQL & Redis Exposed with Default Credentials — ✅ FIXED

Section titled “HIGH-8: PostgreSQL & Redis Exposed with Default Credentials — ✅ FIXED”
Filedocker-compose.yml
Status✅ Fixed — services bound to 127.0.0.1, passwords via env vars, Redis requirepass + memory limits

HIGH-9: Anonymous Reviews Leak Student Avatar — ✅ FIXED

Section titled “HIGH-9: Anonymous Reviews Leak Student Avatar — ✅ FIXED”
Fileapps/api/src/services/reviews/review-service.ts
Status✅ Fixed — reviewerAvatarUrl cleared on anonymous reviews (create, update, and public API sanitization)

HIGH-10: GDPR Erasure Deadline Not Enforced — ✅ FIXED

Section titled “HIGH-10: GDPR Erasure Deadline Not Enforced — ✅ FIXED”
Fileapps/api/src/services/privacy/erasure-request.ts
Status✅ Fixed — overdue requests logged with GDPR COMPLIANCE WARNING via Pino structured logging

HIGH-11: Soft-Deleted Students Still Visible in Session Queries — ✅ FIXED

Section titled “HIGH-11: Soft-Deleted Students Still Visible in Session Queries — ✅ FIXED”
Fileapps/api/src/services/scheduling/session-service.ts
Status✅ Fixed — anonymizeSoftDeletedStudents() helper strips PII from deleted students in 5 query methods

HIGH-12: Missing Price Amount Validation Before Stripe Checkout — ✅ FIXED

Section titled “HIGH-12: Missing Price Amount Validation Before Stripe Checkout — ✅ FIXED”
Fileapps/api/src/services/billing/enrollment-service.ts
Status✅ Fixed — priceAmount > 0 validation before Stripe checkout creation

IDIssueFileStatus
MED-1CORS origin not validated for HTTPSapp.ts✅ Fixed — production restricts to APP_URL, warns if not HTTPS
MED-2Global rate limit too high (100/min)app.ts✅ Fixed — reduced to 60/min
MED-3SameSite=Lax instead of Strictsession-cookie.tsWon’t fix — Lax is correct for OAuth redirects
MED-4No HTTPS redirect enforcementapp.ts✅ Fixed — onRequest hook redirects HTTP→HTTPS in production
MED-5Magic link token in URL query parammagic-link-service.ts✅ Fixed — DB stores SHA-256 hash, URL token is opaque
MED-6Magic link token stored in plaintextmagic-link-service.ts✅ Fixed — crypto.createHash('sha256') before storing
MED-7console.log in production workersMultiple worker files✅ Fixed — replaced with Pino structured logging
MED-8Avatar MIME type not strictly validatedprofile-service.ts✅ Fixed — allowlist: jpeg/png/webp/gif, SVG rejected
MED-9Credit ledger amounts not validatedcredit-ledger-service.ts✅ Fixed — validateAmount() checks positive integer
MED-10Session payment records not in GDPR erasurestudent-data-erasure.ts✅ Fixed — Stripe IDs anonymized, amounts preserved
MED-11No pre-booking consent for data processingclass-session.ts✅ Fixed — consentAccepted: z.literal(true) required
MED-12Audit logs over-collect PII in changes fieldaudit-service.ts✅ Fixed — PII fields redacted to [redacted] in diffs
MED-13Student phone exposed in session detailsession-service.ts✅ Fixed — phone removed from session query columns
MED-14Env vars validated lazily (runtime crash)env.ts✅ Fixed — requireEnv() validates at startup

IDIssueFileStatus
LOW-1Impersonation session 1h without sliding expirysession-service.ts✅ Fixed — reduced to 30 min, fixed expiry (no sliding)
LOW-2No session enumeration timing protectionsession-service.ts✅ Fixed — session ID format validation (64 hex chars) before DB query
LOW-3Google token refresh not validated before storinggoogle-client.ts✅ Fixed — validates access_token exists before persisting
LOW-4Unvalidated query params in profile routesroutes/teacher/profile.ts✅ Fixed — Zod schemas with z.coerce.number().int().min(1).max(365)
LOW-5request.body as any in student update routeroutes/teacher/students-mgmt.ts✅ Fixed — uses updateStudentSchema.parse()
LOW-6Seed SQL files in git working directory.gitignore✅ Fixed — seed-*.sql added to .gitignore
LOW-7Notification logs not cleaned by retention workerdata-retention-worker.ts✅ Fixed — cleanup added matching contact_log retention
LOW-8Drive files marked deleted but not actually removedstudent-data-erasure.ts✅ Fixed — GoogleDriveService.trashFile() called post-transaction

The audit also confirmed strong security in several areas:

  • SQL Injection: Drizzle ORM parameterizes all queries safely (the one sql.raw() finding is now fixed)
  • XSS: React safe rendering throughout, react-markdown sanitizes by default, no dangerouslySetInnerHTML
  • Command Injection: No exec(), spawn(), or child_process usage with user input
  • Path Traversal: No filesystem operations with user input (Google Drive used instead)
  • Stripe Webhook Signature: constructEvent() properly verifies signatures (secret now required)
  • Credit Idempotency: idempotencyKey with partial UNIQUE index prevents duplicate credits
  • Cookie Security: httpOnly: true, sameSite: 'lax', secure in production
  • Rate Limiting: Auth endpoints properly rate-limited (magic link 5/hour, verify 10/min, dev-login 5/hour)
  • Audit Trail: Comprehensive append-only audit logging with IP, user agent, request ID
  • Soft-Delete: 7 tables use deletedAt pattern with notDeleted() helper
  • Zod Validation: 33+ schema files validate all API input on both frontend and backend
  • Security Headers: @fastify/helmet provides HSTS, X-Frame-Options, X-Content-Type-Options, etc.
  • OAuth CSRF: State parameter with Redis-backed single-use tokens

GDPR ArticleStatusNotes
Art. 5 — Data MinimizationCompliantAvatar leak fixed, phone removed from sessions, audit PII redacted
Art. 7 — ConsentCompliantLegal doc acceptance tracked, pre-booking consent required
Art. 15 — Right to AccessCompliantExport includes all data correctly
Art. 17 — Right to ErasureCompliantDeadline warnings logged, soft-deleted students anonymized, session_payment included
Art. 25 — Data Protection by DesignCompliantIDOR fixes, env validation, security headers, magic link hashing
Art. 32 — Security of ProcessingCompliantSQL injection fixed, IDOR fixed, databases secured, HTTPS enforced
Art. 5 — Storage LimitationCompliantRetention tiers well-designed, notification logs cleanup added

After remediation, verify with:

  1. IDOR testing: For every teacher endpoint, attempt access with a different teacher’s session
  2. SQL injection: Fuzz all sql.raw() and sql template usages
  3. Webhook replay: Send duplicate webhooks with same event ID concurrently
  4. OAuth CSRF: Attempt callback without valid state parameter
  5. Rate limit bypass: Test concurrent requests from different IPs
  6. GDPR erasure: Run full erasure and verify ALL tables are cleaned
  7. Soft-delete leak: Query sessions after student erasure, verify no PII returned