Last updated: 2026-03-04
Tip
All 6 Critical , 12 High , 13 Medium , and 8 Low severity findings have been remediated (1 Medium marked “won’t fix”). See status column in each section.
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.
File apps/api/src/services/teacher/retention-metrics-service.tsType SQL Injection CVSS 9.8 Status ✅ Fixed — replaced sql.raw() with Drizzle’s inArray() operator
File apps/api/src/services/auth/google-oauth.ts, apps/api/src/routes/auth/index.tsType CSRF / Account Takeover Status ✅ Fixed — crypto.randomBytes(32) state stored in Redis (10-min TTL), validated and deleted on callback
File apps/api/src/services/billing/enrollment-service.tsRoutes GET /teacher/enrollments/:id, POST /teacher/enrollments/:id/cancelStatus ✅ Fixed — teacherId parameter added to getEnrollmentById() and cancelEnrollment(), all callers updated
File apps/api/src/services/scheduling/session-participant-service.tsRoutes GET/POST/DELETE /teacher/sessions/:id/participantsStatus ✅ Fixed — verifySessionOwnership() helper added, teacherId parameter in all public methods, all callers updated
File apps/api/src/services/scheduling/service-schedule-service.tsRoute GET /teacher/services/:id/scheduleStatus ✅ Fixed — listScheduleItems() now requires teacherId and verifies service ownership
File apps/api/src/services/billing/enrollment-service.tsType Broken Access Control Status ✅ Fixed — student.teacherId !== service.teacherId check added before enrollment creation
File apps/api/src/lib/env.tsStatus ✅ Fixed — requireEnv() helper crashes on startup if SESSION_SECRET is missing in production. Dev fallback only in non-production.
File apps/api/src/services/billing/stripe-service.ts, apps/api/src/lib/env.tsStatus ✅ Fixed — STRIPE_WEBHOOK_SECRET required via requireEnv(), crashes on missing in production. Uses env.STRIPE_WEBHOOK_SECRET instead of process.env.
File apps/api/src/routes/webhooks/index.tsStatus ✅ Fixed — replaced SELECT-then-INSERT with atomic INSERT ... ON CONFLICT DO NOTHING, checks returning() for dedup
File apps/api/src/routes/webhooks/index.tsStatus ✅ Fixed — on handler failure, webhook record is deleted and 500 is returned so Stripe retries
File apps/api/src/routes/auth/index.tsStatus ✅ Fixed — rate limit (5/hour) and audit logging added to dev-login route
File apps/api/src/services/auth/session-service.tsStatus ✅ Fixed — invalidateAllSessionsExcept(teacherId, currentSessionId) method added
File apps/api/src/app.tsStatus ✅ Fixed — @fastify/helmet installed with HSTS (production), X-Frame-Options, X-Content-Type-Options, Referrer-Policy
File docker-compose.ymlStatus ✅ Fixed — services bound to 127.0.0.1, passwords via env vars, Redis requirepass + memory limits
File apps/api/src/services/reviews/review-service.tsStatus ✅ Fixed — reviewerAvatarUrl cleared on anonymous reviews (create, update, and public API sanitization)
File apps/api/src/services/privacy/erasure-request.tsStatus ✅ Fixed — overdue requests logged with GDPR COMPLIANCE WARNING via Pino structured logging
File apps/api/src/services/scheduling/session-service.tsStatus ✅ Fixed — anonymizeSoftDeletedStudents() helper strips PII from deleted students in 5 query methods
File apps/api/src/services/billing/enrollment-service.tsStatus ✅ 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.tsWon’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 workersMultiple 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 routeroutes/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:
IDOR testing: For every teacher endpoint, attempt access with a different teacher’s session
SQL injection: Fuzz all sql.raw() and sql template usages
Webhook replay: Send duplicate webhooks with same event ID concurrently
OAuth CSRF: Attempt callback without valid state parameter
Rate limit bypass: Test concurrent requests from different IPs
GDPR erasure: Run full erasure and verify ALL tables are cleaned
Soft-delete leak: Query sessions after student erasure, verify no PII returned