Skip to content

Security Audit

Last updated: 2026-03-04


Domain Critical High Medium Low
Authentication & Sessions 2 ✅ 4 ✅ 4 4
Input Validation & Injection 1 ✅ 0 3 0
Payments & Billing 0 3 ✅ 4 0
Authorization & IDOR 3 ✅ 3 ✅ 1 2
Infrastructure & Config 0 4 ✅ 4 1
GDPR & Privacy 0 3 ✅ 5 3
Total 6 ✅ 12 ✅ 14 (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”
File apps/api/src/services/teacher/retention-metrics-service.ts
Type SQL Injection
CVSS 9.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”
File apps/api/src/services/auth/google-oauth.ts, apps/api/src/routes/auth/index.ts
Type CSRF / 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”
File apps/api/src/services/billing/enrollment-service.ts
Routes GET /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”
File apps/api/src/services/scheduling/session-participant-service.ts
Routes GET/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”
File apps/api/src/services/scheduling/service-schedule-service.ts
Route GET /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”
File apps/api/src/services/billing/enrollment-service.ts
Type Broken 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”
File apps/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”
File apps/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”
File apps/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”
File apps/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”
File apps/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”
File apps/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”
File apps/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”
File docker-compose.yml (retirado el 1-oct-2026 con Neon)
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”
File apps/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”
File apps/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”
File apps/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”
File apps/api/src/services/billing/enrollment-service.ts
Status ✅ Fixed — priceAmount > 0 validation before Stripe checkout creation

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

ID Issue File Status
LOW-1 Impersonation session 1h without sliding expiry session-service.ts ✅ Fixed — reduced to 30 min, fixed expiry (no sliding)
LOW-2 No session enumeration timing protection session-service.ts ✅ Fixed — session ID format validation (64 hex chars) before DB query
LOW-3 Google token refresh not validated before storing google-client.ts ✅ Fixed — validates access_token exists before persisting
LOW-4 Unvalidated query params in profile routes routes/teacher/profile.ts ✅ Fixed — Zod schemas with z.coerce.number().int().min(1).max(365)
LOW-5 request.body as any in student update route routes/teacher/students-mgmt.ts ✅ Fixed — uses updateStudentSchema.parse()
LOW-6 Seed SQL files in git working directory .gitignore ✅ Fixed — seed-*.sql added to .gitignore
LOW-7 Notification logs not cleaned by retention worker data-retention-worker.ts ✅ Fixed — cleanup added matching contact_log retention
LOW-8 Drive files marked deleted but not actually removed student-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 Article Status Notes
Art. 5 — Data Minimization Compliant Avatar leak fixed, phone removed from sessions, audit PII redacted
Art. 7 — Consent Compliant Legal doc acceptance tracked, pre-booking consent required
Art. 15 — Right to Access Compliant Export includes all data correctly
Art. 17 — Right to Erasure Compliant Deadline warnings logged, soft-deleted students anonymized, session_payment included
Art. 25 — Data Protection by Design Compliant IDOR fixes, env validation, security headers, magic link hashing
Art. 32 — Security of Processing Compliant SQL injection fixed, IDOR fixed, databases secured, HTTPS enforced
Art. 5 — Storage Limitation Compliant Retention 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