# Security Audit Report - spotlight.cam Backend **Date:** 2025-11-13 **Auditor:** Security Review **Scope:** Backend API (Node.js/Express) **Framework:** OWASP Top 10 2021 --- ## Executive Summary This security audit identified **21 security issues** across 4 severity levels: - 🔴 **CRITICAL (P0):** 5 issues - Immediate action required - 🟠 **HIGH (P1):** 6 issues - Fix within 1 week - 🟡 **MEDIUM (P2):** 7 issues - Fix within 2-4 weeks - 🔵 **LOW (P3):** 3 issues - Fix when convenient **Overall Security Rating:** ⚠️ **MODERATE RISK** --- ## 🔴 CRITICAL Issues (P0) - FIX IMMEDIATELY ### 1. Secrets Exposed in `.env` File (CWE-798) **Severity:** 🔴 CRITICAL **OWASP:** A02:2021 - Cryptographic Failures **File:** `backend/.env` **Issue:** ```bash JWT_SECRET=dev-secret-key-12345-change-in-production AWS_ACCESS_KEY_ID=your-aws-access-key-id AWS_SECRET_ACCESS_KEY=your-aws-secret-access-key DATABASE_URL=postgresql://spotlightcam:spotlightcam123@db:5432/spotlightcam ``` **Vulnerabilities:** - Weak JWT secret key - Default placeholder AWS credentials - Database password in plain text - `.env` file may be committed to git **Impact:** - Attacker can forge JWT tokens - Potential unauthorized access to AWS services - Database compromise **Recommendation:** ```bash # Use strong random secrets (at least 32 characters) JWT_SECRET=$(openssl rand -base64 32) # Use AWS IAM roles instead of access keys (in production) # Or use AWS Secrets Manager # Use environment variables in production (Kubernetes secrets, Docker secrets, etc.) # Never commit .env to git - add to .gitignore ``` **Action:** 1. Generate strong JWT secret: `openssl rand -base64 64` 2. Use AWS IAM roles or Secrets Manager 3. Verify `.env` is in `.gitignore` 4. Rotate all secrets immediately if `.env` was committed --- ### 2. Insecure Random Number Generation (CWE-338) **Severity:** 🔴 CRITICAL **OWASP:** A02:2021 - Cryptographic Failures **File:** `backend/src/utils/auth.js:38` **Issue:** ```javascript function generateVerificationCode() { return Math.floor(100000 + Math.random() * 900000).toString(); } ``` **Vulnerabilities:** - `Math.random()` is NOT cryptographically secure - Predictable verification codes - Can be brute-forced **Impact:** - Attacker can predict verification codes - Account takeover possible **Recommendation:** ```javascript const crypto = require('crypto'); function generateVerificationCode() { // Cryptographically secure random 6-digit code const bytes = crypto.randomBytes(4); const num = bytes.readUInt32BE(0); return String(num % 900000 + 100000); } ``` --- ### 3. No Rate Limiting - Brute Force Vulnerability (CWE-307) **Severity:** 🔴 CRITICAL **OWASP:** A07:2021 - Identification and Authentication Failures **File:** `backend/src/app.js` **Issue:** No rate limiting on any endpoints. **Vulnerabilities:** - Login brute force attacks - Password reset abuse - Verification code brute force (6 digits = 1M combinations) - Email bombing via resend verification **Impact:** - Account takeover - Service disruption (DoS) - Email service abuse **Recommendation:** ```bash npm install express-rate-limit ``` ```javascript // backend/src/app.js const rateLimit = require('express-rate-limit'); // General API rate limiter const apiLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 100, // limit each IP to 100 requests per windowMs message: 'Too many requests from this IP, please try again later.', standardHeaders: true, legacyHeaders: false, }); // Strict limiter for auth endpoints const authLimiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutes max: 5, // 5 attempts skipSuccessfulRequests: true, message: 'Too many login attempts, please try again later.', }); // Email limiter const emailLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hour max: 3, // 3 emails per hour message: 'Too many emails sent, please try again later.', }); app.use('/api/', apiLimiter); app.use('/api/auth/login', authLimiter); app.use('/api/auth/register', authLimiter); app.use('/api/auth/resend-verification', emailLimiter); app.use('/api/auth/request-password-reset', emailLimiter); ``` --- ### 4. No Request Body Size Limit - DoS Vulnerability (CWE-400) **Severity:** 🔴 CRITICAL **OWASP:** A05:2021 - Security Misconfiguration **File:** `backend/src/app.js:11` **Issue:** ```javascript app.use(express.json()); // No limit app.use(express.urlencoded({ extended: true })); // No limit ``` **Vulnerabilities:** - Attacker can send huge JSON payloads - Memory exhaustion (DoS) **Impact:** - Server crash - Service unavailability **Recommendation:** ```javascript app.use(express.json({ limit: '10kb' })); app.use(express.urlencoded({ extended: true, limit: '10kb' })); ``` --- ### 5. User Enumeration Vulnerability (CWE-204) **Severity:** 🔴 CRITICAL **OWASP:** A01:2021 - Broken Access Control **File:** `backend/src/controllers/auth.js:28-46` **Issue:** ```javascript if (existingUser.email === email) { return res.status(400).json({ error: 'Email already registered', }); } if (existingUser.username === username) { return res.status(400).json({ error: 'Username already taken', }); } ``` **Vulnerabilities:** - Reveals which emails/usernames are registered - Enables targeted attacks **Impact:** - Email/username enumeration - Privacy breach - Targeted phishing attacks **Recommendation:** ```javascript // Don't reveal which field exists if (existingUser) { return res.status(400).json({ success: false, error: 'An account with these credentials already exists', }); } // Or implement verification via email before confirming registration ``` --- ## 🟠 HIGH Issues (P1) - Fix Within 1 Week ### 6. Weak Password Policy (CWE-521) **Severity:** 🟠 HIGH **File:** `backend/src/middleware/validators.js:30` **Issue:** ```javascript body('password') .isLength({ min: 6 }) // Too weak! ``` And in `auth.js:441`: ```javascript if (newPassword.length < 8) // Inconsistent ``` **Vulnerabilities:** - 6 characters is too weak (should be 8+) - No complexity requirements - Inconsistent validation (6 vs 8) **Recommendation:** ```javascript body('password') .isLength({ min: 8, max: 128 }) .withMessage('Password must be between 8 and 128 characters') .matches(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/) .withMessage('Password must contain at least one uppercase, one lowercase, and one number'), ``` --- ### 7. Missing Security Headers (CWE-693) **Severity:** 🟠 HIGH **OWASP:** A05:2021 - Security Misconfiguration **File:** `backend/src/app.js` **Issue:** No security headers (CSP, HSTS, X-Frame-Options, etc.) **Recommendation:** ```bash npm install helmet ``` ```javascript const helmet = require('helmet'); app.use(helmet({ contentSecurityPolicy: { directives: { defaultSrc: ["'self'"], styleSrc: ["'self'", "'unsafe-inline'"], scriptSrc: ["'self'"], imgSrc: ["'self'", "data:", "https:"], }, }, hsts: { maxAge: 31536000, includeSubDomains: true, preload: true, }, })); ``` --- ### 8. No Account Lockout After Failed Logins (CWE-307) **Severity:** 🟠 HIGH **File:** `backend/src/controllers/auth.js:120` **Issue:** No account lockout mechanism after multiple failed login attempts. **Recommendation:** Add to database schema: ```prisma model User { failedLoginAttempts Int @default(0) lockedUntil DateTime? } ``` Implement lockout logic: ```javascript // In login controller if (user.lockedUntil && user.lockedUntil > new Date()) { return res.status(423).json({ error: 'Account temporarily locked due to too many failed login attempts', lockedUntil: user.lockedUntil, }); } if (!isPasswordValid) { await prisma.user.update({ where: { id: user.id }, data: { failedLoginAttempts: { increment: 1 }, ...(user.failedLoginAttempts + 1 >= 5 && { lockedUntil: new Date(Date.now() + 15 * 60 * 1000), // 15 minutes }), }, }); return res.status(401).json({ error: 'Invalid credentials' }); } // Reset on successful login await prisma.user.update({ where: { id: user.id }, data: { failedLoginAttempts: 0, lockedUntil: null }, }); ``` --- ### 9. Missing Input Validation for WSDC Data (CWE-20) **Severity:** 🟠 HIGH **File:** `backend/src/controllers/auth.js:15` **Issue:** No validation for `firstName`, `lastName`, `wsdcId` in register. **Recommendation:** ```javascript // In validators.js body('firstName') .optional() .trim() .isLength({ max: 100 }) .matches(/^[a-zA-Z\s'-]+$/) .withMessage('Invalid first name'), body('lastName') .optional() .trim() .isLength({ max: 100 }) .matches(/^[a-zA-Z\s'-]+$/) .withMessage('Invalid last name'), body('wsdcId') .optional() .trim() .matches(/^\d{1,10}$/) .withMessage('WSDC ID must be numeric (max 10 digits)'), ``` --- ### 10. No Email Input Sanitization - XSS Risk (CWE-79) **Severity:** 🟠 HIGH **OWASP:** A03:2021 - Injection **File:** `backend/src/utils/email.js:98` **Issue:** ```javascript

Hi ${firstName || 'there'}! 👋

``` If `firstName` contains ``, it's injected into email. **Recommendation:** ```bash npm install dompurify jsdom ``` ```javascript const createDOMPurify = require('dompurify'); const { JSDOM } = require('jsdom'); const window = new JSDOM('').window; const DOMPurify = createDOMPurify(window); // Sanitize before using in email const sanitizedFirstName = DOMPurify.sanitize(firstName || 'there', { ALLOWED_TAGS: [], }); ``` --- ### 11. Missing CSRF Protection (CWE-352) **Severity:** 🟠 HIGH **OWASP:** A01:2021 - Broken Access Control **File:** `backend/src/app.js` **Issue:** No CSRF token validation for state-changing operations. **Recommendation:** ```bash npm install csurf cookie-parser ``` ```javascript const csrf = require('csurf'); const cookieParser = require('cookie-parser'); app.use(cookieParser()); app.use(csrf({ cookie: true })); // Add endpoint to get CSRF token app.get('/api/csrf-token', (req, res) => { res.json({ csrfToken: req.csrfToken() }); }); // Frontend must include CSRF token in requests // Headers: { 'X-CSRF-Token': token } ``` --- ## 🟡 MEDIUM Issues (P2) - Fix Within 2-4 Weeks ### 12. Permissive CORS Configuration (CWE-346) **Severity:** 🟡 MEDIUM **File:** `backend/src/app.js:7` **Issue:** ```javascript app.use(cors({ origin: process.env.CORS_ORIGIN || 'http://localhost:8080', credentials: true })); ``` In production, this could be misconfigured. **Recommendation:** ```javascript const allowedOrigins = [ 'https://spotlight.cam', 'https://www.spotlight.cam', ...(process.env.NODE_ENV === 'development' ? ['http://localhost:8080'] : []), ]; app.use(cors({ origin: (origin, callback) => { if (!origin || allowedOrigins.includes(origin)) { callback(null, true); } else { callback(new Error('Not allowed by CORS')); } }, credentials: true, maxAge: 86400, // 24 hours })); ``` --- ### 13. Error Information Disclosure (CWE-209) **Severity:** 🟡 MEDIUM **File:** `backend/src/app.js:47` **Issue:** ```javascript res.status(err.status || 500).json({ error: err.message || 'Internal Server Error', ...(process.env.NODE_ENV === 'development' && { stack: err.stack }) }); ``` Stack traces leak in development if `NODE_ENV` not set in production. **Recommendation:** ```javascript app.use((err, req, res, next) => { console.error('Error:', err); // Log full error internally logger.error(err); // Return generic error to client in production const isDevelopment = process.env.NODE_ENV === 'development'; res.status(err.status || 500).json({ error: isDevelopment ? err.message : 'Internal Server Error', ...(isDevelopment && { stack: err.stack }), }); }); ``` --- ### 14. No Validation on Email Verification Endpoints **Severity:** 🟡 MEDIUM **File:** `backend/src/controllers/auth.js:233` **Issue:** No input validation for `verifyEmailByCode`. **Recommendation:** Add validation: ```javascript body('email').trim().isEmail().normalizeEmail(), body('code').trim().matches(/^\d{6}$/).withMessage('Code must be 6 digits'), ``` --- ### 15. No Logging for Security Events (CWE-778) **Severity:** 🟡 MEDIUM **File:** All controllers **Issue:** Only `console.log()` used. No structured logging. **Recommendation:** ```bash npm install winston ``` ```javascript const winston = require('winston'); const logger = winston.createLogger({ level: 'info', format: winston.format.json(), transports: [ new winston.transports.File({ filename: 'error.log', level: 'error' }), new winston.transports.File({ filename: 'security.log', level: 'warn' }), new winston.transports.File({ filename: 'combined.log' }), ], }); // Log security events logger.warn('Failed login attempt', { email, ip: req.ip }); logger.warn('Account locked', { userId, ip: req.ip }); ``` --- ### 16. Missing JWT Token Blacklist/Revocation (CWE-613) **Severity:** 🟡 MEDIUM **File:** `backend/src/utils/auth.js` **Issue:** No way to revoke JWT tokens before expiry. **Recommendation:** Implement token blacklist using Redis: ```javascript // When user logs out or changes password await redis.setex(`blacklist:${token}`, 86400, '1'); // In auth middleware const isBlacklisted = await redis.get(`blacklist:${token}`); if (isBlacklisted) { return res.status(401).json({ error: 'Token revoked' }); } ``` --- ### 17. No Password History Check (CWE-521) **Severity:** 🟡 MEDIUM **File:** `backend/src/controllers/auth.js:429` **Issue:** Users can reuse old passwords. **Recommendation:** Add password history tracking (prevent reuse of last 5 passwords). --- ### 18. Timing Attack Vulnerability in Token Comparison (CWE-208) **Severity:** 🟡 MEDIUM **File:** `backend/src/controllers/auth.js:178` **Issue:** String comparison for tokens may leak timing information. **Recommendation:** ```javascript const crypto = require('crypto'); function timingSafeEqual(a, b) { if (typeof a !== 'string' || typeof b !== 'string') return false; if (a.length !== b.length) return false; return crypto.timingSafeEqual( Buffer.from(a, 'utf8'), Buffer.from(b, 'utf8') ); } // Use in comparisons if (!timingSafeEqual(user.verificationToken, token)) { return res.status(404).json({ error: 'Invalid token' }); } ``` --- ## 🔵 LOW Issues (P3) - Fix When Convenient ### 19. No Security Monitoring/Alerts **Severity:** 🔵 LOW **Recommendation:** Implement monitoring for: - Multiple failed login attempts - Password reset requests - Account lockouts - Unusual API usage patterns --- ### 20. Missing HTTP Strict Transport Security (HSTS) **Severity:** 🔵 LOW **Recommendation:** Add via helmet (see issue #7). --- ### 21. No Password Strength Meter Feedback **Severity:** 🔵 LOW **Recommendation:** Implement server-side password strength validation: ```bash npm install zxcvbn ``` ```javascript const zxcvbn = require('zxcvbn'); const result = zxcvbn(password); if (result.score < 3) { return res.status(400).json({ error: 'Password too weak', suggestions: result.feedback.suggestions, }); } ``` --- ## Security Best Practices Checklist - [ ] Use environment-specific secrets - [ ] Implement rate limiting on all endpoints - [ ] Use helmet.js for security headers - [ ] Enable CSRF protection - [ ] Add request body size limits - [ ] Implement account lockout mechanism - [ ] Use cryptographically secure random generation - [ ] Add input validation for all endpoints - [ ] Sanitize user inputs (XSS prevention) - [ ] Implement structured logging - [ ] Add security monitoring and alerts - [ ] Use HTTPS in production - [ ] Implement JWT token revocation - [ ] Add password strength requirements - [ ] Use timing-safe comparisons for sensitive data - [ ] Prevent user enumeration - [ ] Implement proper CORS configuration - [ ] Regular security audits - [ ] Dependency vulnerability scanning (`npm audit`) - [ ] Keep dependencies up to date --- ## Immediate Action Items (This Week) 1. **Generate and rotate all secrets** 2. **Install and configure rate limiting** (`express-rate-limit`) 3. **Install and configure helmet** (`helmet`) 4. **Fix cryptographic random generation** in `generateVerificationCode()` 5. **Add request body size limits** 6. **Strengthen password requirements** (8+ chars, complexity) 7. **Fix user enumeration** in registration 8. **Add input validation** for all fields --- ## References - OWASP Top 10 2021: https://owasp.org/Top10/ - Node.js Security Checklist: https://cheatsheetseries.owasp.org/cheatsheets/Nodejs_Security_Cheat_Sheet.html - Express Security Best Practices: https://expressjs.com/en/advanced/best-practice-security.html --- **Report Generated:** 2025-11-13 **Next Audit Recommended:** 2025-12-13 (or after major changes)