Skip to content

Security: baishnvi/InterviewAi

Security

SECURITY.md

InterviewAI — Security Overview

This document covers what's actually implemented, why, and what's explicitly out of scope for a portfolio project.

Authentication & session security

  • Passwords hashed with bcrypt (cost factor 12), never stored or logged in plaintext
  • JWT access tokens (15 min) + refresh tokens (7 days), both httpOnly — never touched by JavaScript, so they're not readable by an XSS payload even if one somehow got through
  • Refresh token rotation: every refresh revokes the old session and issues a new one, tracked in a Session collection with a TTL index for automatic expiry cleanup
  • tokenVersion on the User model: changing your password immediately invalidates every other refresh token in existence, not just the one used to change it
  • Account lockout: 5 failed login attempts locks the account for 15 minutes, independent of the IP-based rate limiter below — this protects against distributed credential-stuffing attempts that spread requests across many IPs to dodge per-IP limits
  • Login/signup/password-reset error messages are deliberately generic ("Invalid email or password") so they don't reveal whether a given email is registered

CSRF (Cross-Site Request Forgery)

Cookie-based auth (vs. bearer tokens in localStorage) trades one class of risk (XSS token theft) for another (CSRF), so this needed explicit handling:

  • All state-changing requests (POST/PUT/PATCH/DELETE) require a custom X-Requested-With: XMLHttpRequest header (src/middleware/csrf.middleware.ts)
  • Plain HTML forms — including multipart/form-data file-upload forms, which are exempt from CORS preflight and would otherwise be a CSRF hole even with strict CORS — cannot set custom headers, so a forged cross-site form submission never reaches this header check
  • The frontend's axios instance sends this header by default on every request, so this is invisible to normal usage

CORS

  • Locked to a single configured origin (CLIENT_URL), not a wildcard, with credentials: true
  • Explicit methods and allowedHeaders allowlists

Input validation & sanitization

  • Every request body/query/params validated with Zod schemas before reaching a controller
  • express-mongo-sanitize strips $/. keys from input (NoSQL injection protection)
  • hpp blocks HTTP parameter pollution (duplicate query keys)
  • User-supplied free text that gets stored and later rendered elsewhere (name, interview role, tech stack, resume-extracted text) is stripped of any HTML via sanitize-html before it's saved — defense-in-depth beyond React's default JSX escaping, since this text also flows into PDF reports and the admin panel

Rate limiting

  • Global limiter on all /api routes
  • A stricter limiter specifically on auth routes (signup/login/password-reset), separate from the account-lockout mechanism above — this covers the case where an attacker spreads attempts across many different accounts from one IP, which per-account lockout alone wouldn't catch

File uploads

  • multer (upgraded to 2.x during this hardening pass — 1.x had several disclosed vulnerabilities) with memory storage, so files never touch disk on the API server
  • Resume uploads: PDF/DOCX only (MIME-type allowlist), 5MB cap
  • Voice answer uploads: audio MIME types only, 20MB cap
  • Both go through Cloudinary, not local storage

HTTP security headers

  • helmet with an explicit Content-Security-Policy (default-src 'self', no inline scripts/styles, images allowed from Cloudinary) rather than relying on helmet's defaults alone
  • app.set('trust proxy', 1) — required for correct client IP detection (rate limiting) and secure cookies behind Railway/most PaaS reverse proxies

Dependency hygiene

Ran npm audit on both backend and frontend as part of this hardening pass and fixed everything it found:

  • nodemailer (had multiple disclosed CVEs including SMTP command injection) → upgraded to 9.x
  • bcrypt → upgraded to 6.x (pulled in a patched tar transitively)
  • multer → upgraded to 2.x
  • Both projects show 0 vulnerabilities as of this pass

Secrets

  • All secrets (JWT_ACCESS_SECRET, JWT_REFRESH_SECRET, SMTP credentials, Cloudinary keys, OpenAI key, MongoDB URI) load from environment variables, validated with Zod at boot (src/config/env.ts) — the app refuses to start with missing/malformed config rather than failing unpredictably later
  • .env is gitignored in both projects; only .env.example (with placeholder values) is committed

What's explicitly out of scope

Being upfront about what a portfolio project like this doesn't need to solve:

  • WAF / DDoS protection — handled at the infrastructure layer (Cloudflare/Railway/Vercel), not application code
  • Automated dependency scanning in CInpm audit was run manually as part of this pass; a real production app would wire this into CI (Dependabot, Snyk, etc.)
  • Penetration testing — everything above was verified via direct testing in this environment (see each phase's README for what was actually run), not a professional security audit
  • 2FA/MFA — not implemented; would be a reasonable v2 addition
  • Content moderation on resume uploads / interview answers — no scanning for malicious file content beyond MIME-type + size checks

Quick self-check if you're extending this

  • New free-text field being stored? Run it through sanitizeText() before saving
  • New state-changing route? It's automatically covered by the CSRF middleware — no per-route action needed, just make sure your frontend client sends X-Requested-With
  • New file upload endpoint? Use multer.memoryStorage(), set an explicit MIME allowlist and size cap, never write to local disk

There aren't any published security advisories