07-security-analysis.md

Security Analysis — Security Decisions

Authentication

How Identity Is Verified

  • Method: JWT (JSON Web Tokens) with HS256 symmetric signing
  • Library: jsonwebtoken v9
  • Secret: process.env.JWT_SECRET with hardcoded fallback "mysecret" (jwtToken.js L13)
  • Expiry: 24h for session tokens, 1h for password reset tokens

Token Storage Strategy

  • Client-side: localStorage.setItem('token', token) in browser
  • Sent as: Authorization: Bearer <token> header on every API request
  • Parsed on server: req.headers.authorization.split(" ")[1]

⚠ Token Storage Vulnerability

localStorage is accessible to any JavaScript running on the page. If there's an XSS vulnerability, an attacker can steal the JWT and impersonate the user. httpOnly cookies would be more secure but are incompatible with the current architecture (different origins for frontend/backend).

⚠ Fallback JWT Secret

// jwtToken.js L8-16 const getJwtSecret = () => { if (!process.env.JWT_SECRET) { console.warn("WARNING: JWT_SECRET is not set..."); return "mysecret"; // ← Anyone who reads the source can forge tokens } return process.env.JWT_SECRET; };

If JWT_SECRET is not set in production, all tokens are signed with "mysecret". An attacker can create { id: 1, role: "doctor", email: "admin@example.com" } and access any doctor's data.


Authorization

How Roles/Permissions Are Enforced

Authorization checks are inline in each route handler:

// Pattern used in most handlers: if (req.user.role !== 'doctor') { return res.status(403).json({ message: "Unauthorized: Only doctors can..." }); }

Role Matrix

EndpointDoctorPatientPublic
Auth (signup/signin/reset)——✅
/doctor/*✅❌ (403)❌ (401)
/patient/*❌ (403)✅❌ (401)
/appointments (create)❌✅❌
/appointments/update-status✅ (any status)✅ (cancel only)❌
/chats, /chats/:id/messages✅ (own chats)✅ (own chats)❌
/api/medicines——✅
/admin/send-medication-reminders——✅ (⚠ NO AUTH!)

⚠ Missing Authorization Checks

  1. /admin/send-medication-reminders — completely unprotected. Anyone can trigger mass email sends.
  2. GET /doctor/doctors — returns ALL patients with their doctors. No role check. Any authenticated user can see all patient-doctor relationships.
  3. Patient ID in URL — GET /patient/medications/today/:patientId checks req.user.role === 'patient' but doesn't verify that req.user.id === patientId. A patient could theoretically view another patient's medications by changing the URL.

Ownership Verification Gaps

The updateMedicationStatus handler checks:

const isAuthorized = decoded.role === 'patient' && decoded.id == patientId || decoded.role === 'doctor';

Note the == (loose equality) and missing parentheses — this is (patient && id_match) || doctor. Any authenticated doctor can update any patient's medication status, even patients not assigned to them.


Input Validation

Where and How

Validation is manual, per-handler, with if (!field) checks:

// Typical pattern: if (!patientId) { return res.status(400).json({ message: "Patient ID is required" }); }

What's Validated

InputValidated?How
Email format❌ NoRelies on DB @unique to catch duplicates, no format check
Password strength❌ NoAny string accepted, even empty after falsy check
Patient agePartialparseInt(age) — NaN if invalid but stored anyway
Medicine fields✅ Yesmedicines.every(med => med.id && med.name && ...)
Appointment date❌ Nonew Date(appointmentDate) — Invalid Date silently creates garbage
Status values✅ YesChecked against ['scheduled', 'completed', 'cancelled', 'missed']

⚠ No Validation Library

No Zod, Joi, or express-validator. Each handler has its own validation logic, which is inconsistent across the codebase. Some handlers validate thoroughly (prescription), others barely check at all (assign-patient).


SQL/NoSQL Injection Prevention

Current Protection

Prisma ORM parameterizes all queries by default. There are no raw SQL queries in the application code (except the migration file). This effectively prevents SQL injection.

// Prisma automatically parameterizes: await prisma.Patient.findUnique({ where: { email: userInput } // ← Parameterized, not concatenated });

⚠ One Exception

The migration 20250806120000 contains raw SQL, but this only runs during prisma migrate deploy, not at runtime.


XSS Protection

Current State

  • React auto-escapes: JSX expressions {variable} are auto-escaped by React, preventing reflected XSS in rendered content.
  • No dangerouslySetInnerHTML used anywhere in the frontend.
  • Chat messages: Stored as plain text, rendered as text in React — safe from stored XSS.

⚠ Vulnerability

  • Email HTML templates in emailService.js and emailServiceAlert.js use string interpolation:
    html: `<p>Hello ${patientName},</p>`
    If patientName contains <script>alert('xss')</script>, it gets injected into the email HTML. Most email clients strip <script> tags, but some render inline event handlers.

CSRF Protection

Current State

No CSRF protection. The app uses JWT in Authorization headers (not cookies), which is inherently CSRF-resistant because:

  • Browsers don't automatically send Authorization headers on cross-origin requests
  • An attacker would need the JWT value to forge a request

However, if the app ever moves to cookie-based auth, CSRF tokens would be needed.


Password Handling

AspectImplementation
Algorithmbcrypt (via bcryptjs)
Salt rounds10 (industry standard minimum)
SaltAuto-generated by bcrypt (included in hash output)
StorageHashed password in DB, never plain text
Comparisonbcrypt.compare(plaintext, hash) — timing-safe

⚠ Concerns

  • No password strength validation — empty passwords, single-character passwords, or common passwords like "password123" are all accepted.
  • No account lockout — unlimited login attempts. An attacker can brute-force passwords without being blocked.
  • No password history — users can reset to the same password repeatedly.

Sensitive Data

What's Stored

DataEncrypted?Where
Passwords✅ bcrypt hashPatient.password, Doctor.password
Email addresses❌ Plain textPatient.email, Doctor.email
Patient names❌ Plain textPatient.name
Patient age/gender❌ Plain textPatient.age, Patient.gender
Medical diagnoses❌ Plain textMedicalRecord.diagnosis
Medication names❌ Plain textPrescribedMedicine.medicineName, MedicineAdherence.medication
Chat messages❌ Plain textMessage.content
JWT tokens❌ Base64-encoded (not encrypted)Client localStorage + Authorization header

⚠ HIPAA/Healthcare Compliance Concerns

This system stores Protected Health Information (PHI): patient names, email addresses, medical diagnoses, medication records, and appointment history. In the US, this requires HIPAA compliance:

  • Data at rest encryption (not implemented)
  • Data in transit encryption (HTTPS — depends on deployment)
  • Audit logging (not implemented)
  • Access controls with audit trails (partially implemented via role checks, no audit trail)
  • Business Associate Agreements with all vendors (Neon, Gmail, Vercel)

Rate Limiting

Current State

No rate limiting whatsoever.

Attack VectorCurrent Vulnerability
Brute-force loginUnlimited attempts, no lockout
Email spam via /admin/send-medication-remindersUnprotected endpoint, unlimited calls
API abuseAny authenticated user can make unlimited requests
Registration spamNo captcha, no rate limit on /auth/signup

What Should Exist

import rateLimit from 'express-rate-limit'; // Global app.use(rateLimit({ windowMs: 60 * 1000, max: 100 })); // Auth-specific (brute-force protection) const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, max: 5 }); app.use('/auth/signin', authLimiter); app.use('/auth/forgot-password', authLimiter);

HTTPS/TLS

Current State

  • Development: HTTP on localhost (no encryption)
  • Production: Depends entirely on the deployment platform. Vercel provides automatic HTTPS. If deployed elsewhere without HTTPS, all JWT tokens, passwords, and patient data travel in plain text.
  • No HSTS header — browser can be downgraded to HTTP via MITM

Security Headers

Current State

No security headers are set. Missing headers:

HeaderPurposeStatus
X-Content-Type-Options: nosniffPrevent MIME type sniffing❌ Missing
X-Frame-Options: DENYPrevent clickjacking❌ Missing
X-XSS-Protection: 1; mode=blockLegacy XSS protection❌ Missing
Strict-Transport-SecurityForce HTTPS❌ Missing
Content-Security-PolicyPrevent XSS and data injection❌ Missing
Referrer-Policy: no-referrerPrevent token leakage in referrer❌ Missing

Fix: Add helmet middleware:

import helmet from 'helmet'; app.use(helmet());

Vulnerabilities This Project Is Still Exposed To

Critical

  1. Hardcoded JWT fallback secret — if JWT_SECRET env var isn't set, any attacker with source code access can forge tokens.
  2. No rate limiting on login — brute-force attacks are trivial.
  3. Unprotected admin endpoint — /admin/send-medication-reminders can be called by anyone.
  4. No HTTPS enforcement — depends on deployment platform.

High

  1. Patient ID guessing — some endpoints don't verify ownership (patient can access other patients' data by changing patientId in URL).
  2. No security headers — susceptible to clickjacking, MIME sniffing, and referrer leakage.
  3. JWT in localStorage — XSS attack can steal session tokens.
  4. No password strength policy — weak passwords accepted.

Medium

  1. No audit logging — no trail of who accessed what patient data.
  2. Email template injection — user names inserted into HTML emails without sanitization.
  3. CORS set to * for Socket.IO — any origin can connect to WebSocket server.
  4. Verbose error messages — some handlers return internal error details to the client (e.g., "Error creating prescription: " + err.message).
  5. No account lockout — unlimited failed login attempts.

Low

  1. Token bloat — entire user profile in JWT payload increases header size.
  2. No token revocation — can't force-logout compromised accounts.
  3. dotenv.config() called in multiple files — race conditions on environment variable loading.