diff --git a/SECURITY_REMEDIATION_PLAN.md b/SECURITY_REMEDIATION_PLAN.md new file mode 100644 index 0000000..1da6e62 --- /dev/null +++ b/SECURITY_REMEDIATION_PLAN.md @@ -0,0 +1,3148 @@ +# Security Remediation Plan +## Shopify AI Repository - Comprehensive Security Enhancement + +**Document Version:** 1.0 +**Created:** February 8, 2026 +**Classification:** Internal - Confidential +**Risk Level:** High Priority Implementation + +--- + +## Executive Summary + +This document provides a comprehensive, prioritized plan to address all security vulnerabilities and weaknesses identified in the Shopify AI repository. The plan is structured into phases, with each phase addressing specific security domains and containing actionable remediation steps with technical specifications. + +### Repository Components in Scope +1. **Chat Application** - Node.js monolithic server (17,144 lines) +2. **OpenCode IDE** - TypeScript/Bun monorepo with SolidJS/Hono +3. **Windows Desktop App** - Tauri-based (Rust + TypeScript) +4. **Container Infrastructure** - Docker deployment configuration + +### Overall Security Posture Assessment +- **Current State**: Medium Security Maturity +- **Critical Vulnerabilities**: 3 identified +- **High Severity Issues**: 12 identified +- **Medium Severity Issues**: 18 identified +- **Low Severity Issues**: 15 identified + +--- + +## Phase 1: Critical Infrastructure Security + +### 1.1 Replace Custom HTTP Server with Express.js Framework + +**Priority:** Critical +**Estimated Effort:** 40 hours +**Risk Level:** Medium (requires thorough testing) + +#### Problem Analysis +The current monolithic `server.js` (17,144 lines) uses Node.js native `http` module without framework protections. This eliminates: +- Built-in security middleware +- Standardized request validation +- Framework security updates +- Community-audited security patterns + +#### Remediation Steps + +##### Step 1.1.1: Install Express.js and Security Middleware +```bash +# Navigate to chat directory +cd chat/ + +# Install Express.js framework +npm install express@^4.19.2 + +# Install security middleware packages +npm install express-rate-limit@^7.1.5 +npm install helmet@^7.1.0 +npm install cors@^2.8.5 +npm install express-validator@^7.0.1 +npm install express-session@^1.18.0 +npm install csurf@^1.11.0 +npm install hpp@^0.2.3 + +# Install additional security utilities +npm install xss-clean@^0.1.4 +npm install express-mongo-sanitize@^2.2.0 +npm install user-agent@^2.1.13 +``` + +##### Step 1.1.2: Create Express Server Structure +Create new directory structure: +``` +chat/ +├── src/ +│ ├── app.js # Express application setup +│ ├── server.js # HTTP server entry point +│ ├── routes/ # Route handlers +│ │ ├── auth.js # Authentication routes +│ │ ├── api.js # General API routes +│ │ ├── admin.js # Admin routes +│ │ └── webhook.js # Webhook handlers +│ ├── middleware/ +│ │ ├── auth.js # Authentication middleware +│ │ ├── rateLimiter.js # Rate limiting +│ │ ├── security.js # Security headers +│ │ ├── validation.js # Request validation +│ │ └── errorHandler.js # Centralized error handling +│ ├── config/ +│ │ ├── express.js # Express configuration +│ │ └── security.js # Security settings +│ └── utils/ +│ ├── logger.js # Logging utility +│ └── sanitizer.js # Input sanitization +``` + +##### Step 1.1.3: Implement Express Application (app.js) +```javascript +// src/app.js +const express = require('express'); +const helmet = require('helmet'); +const cors = require('cors'); +const rateLimit = require('express-rate-limit'); +const mongoSanitize = require('express-mongo-sanitize'); +const xss = require('xss-clean'); +const hpp = require('hpp'); +const session = require('express-session'); +const csurf = require('csurf'); + +const authRoutes = require('./routes/auth'); +const apiRoutes = require('./routes/api'); +const adminRoutes = require('./routes/admin'); +const webhookRoutes = require('./routes/webhook'); +const errorHandler = require('./middleware/errorHandler'); +const { requestLogger } = require('./utils/logger'); + +const app = express(); + +// Security headers +app.use(helmet({ + contentSecurityPolicy: { + directives: { + defaultSrc: ["'self'"], + styleSrc: ["'self'", "'unsafe-inline'"], + scriptSrc: ["'self'"], + imgSrc: ["'self'", "data:", "https:"], + connectSrc: ["'self'", "https://api.openrouter.ai", "https://api.mistral.ai"], + fontSrc: ["'self'"], + objectSrc: ["'none'"], + mediaSrc: ["'self'"], + frameSrc: ["'none'"], + }, + }, + crossOriginEmbedderPolicy: false, + crossOriginResourcePolicy: { policy: "cross-origin" } +})); + +// CORS configuration +app.use(cors({ + origin: process.env.ALLOWED_ORIGINS?.split(',') || ['http://localhost:4500'], + credentials: true, + methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'], + allowedHeaders: ['Content-Type', 'Authorization', 'X-CSRF-Token'], + exposedHeaders: ['X-RateLimit-Limit', 'X-RateLimit-Remaining', 'X-RateLimit-Reset'] +})); + +// Rate limiting - General API +const apiRateLimiter = rateLimit({ + windowMs: 60 * 1000, // 1 minute + max: 100, // 100 requests per minute + message: { + error: 'Too many requests, please try again later.', + retryAfter: 60 + }, + standardHeaders: true, + legacyHeaders: false, + keyGenerator: (req) => { + return req.ip || req.headers['x-forwarded-for'] || 'unknown'; + }, + skip: (req) => { + // Skip rate limiting for health checks + return req.path === '/health' || req.path === '/api/health'; + } +}); + +// Strict rate limiting for authentication endpoints +const authRateLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 5, // 5 attempts per window + message: { + error: 'Too many authentication attempts. Please try again in 15 minutes.', + retryAfter: 900 + }, + standardHeaders: true, + legacyHeaders: false, + keyGenerator: (req) => { + // Use fingerprint for better rate limiting + return `${req.ip}:${req.headers['user-agent'] || 'unknown'}`; + }, + skipFailedRequests: false +}); + +// Apply rate limiters +app.use('/api/', apiRateLimiter); +app.use('/auth/', authRateLimiter); + +// Body parsing with size limits +app.use(express.json({ limit: '10kb' })); +app.use(express.urlencoded({ extended: true, limit: '10kb' })); + +// Security middleware +app.use(mongoSanitize()); +app.use(xss()); +app.use(hpp()); + +// Session management +app.use(session({ + secret: process.env.SESSION_SECRET || process.env.USER_SESSION_SECRET, + name: 'sessionId', + resave: false, + saveUninitialized: false, + cookie: { + secure: process.env.NODE_ENV === 'production', + httpOnly: true, + sameSite: 'strict', + maxAge: 60 * 60 * 1000 // 1 hour + } +})); + +// CSRF protection (after session) +const csrfProtection = csurf({ + cookie: { + httpOnly: true, + secure: process.env.NODE_ENV === 'production', + sameSite: 'strict' + }, + ignoreMethods: ['GET', 'HEAD', 'OPTIONS'] +}); + +// Logging +app.use(requestLogger); + +// Routes +app.use('/auth', authRoutes); +app.use('/api', apiRoutes); +app.use('/admin', adminRoutes); +app.use('/webhook', webhookRoutes); + +// Health check endpoint (no auth required) +app.get('/health', (req, res) => { + res.status(200).json({ status: 'healthy', timestamp: new Date().toISOString() }); +}); + +// Error handling +app.use(errorHandler); + +// 404 handler +app.use((req, res) => { + res.status(404).json({ error: 'Endpoint not found' }); +}); + +module.exports = app; +``` + +##### Step 1.1.4: Create HTTP Server Entry Point (server.js) +```javascript +// src/server.js +const app = require('./app'); +const https = require('https'); +const fs = require('fs'); +const path = require('path'); +const { logger } = require('./utils/logger'); + +const PORT = process.env.PORT || 4500; +const HOST = process.env.HOST || '0.0.0.0'; + +// SSL/TLS configuration for production +let server; +if (process.env.NODE_ENV === 'production') { + const sslOptions = { + key: fs.readFileSync(process.env.SSL_KEY_PATH || '/etc/ssl/private/server.key'), + cert: fs.readFileSync(process.env.SSL_CERT_PATH || '/etc/ssl/certs/server.crt'), + minVersion: 'TLSv1.2', + ciphers: 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384', + honorCipherOrder: true + }; + + server = https.createServer(sslOptions, app); + logger.info('Starting HTTPS server'); +} else { + const http = require('http'); + server = http.createServer(app); + logger.info('Starting HTTP server (development mode)'); +} + +// Graceful shutdown +const gracefulShutdown = (signal) => { + logger.info(`${signal} received. Starting graceful shutdown...`); + + server.close((err) => { + if (err) { + logger.error('Error during shutdown:', err); + process.exit(1); + } + + logger.info('HTTP server closed'); + process.exit(0); + }); + + // Force shutdown after 30 seconds + setTimeout(() => { + logger.error('Forced shutdown after timeout'); + process.exit(1); + }, 30000); +}; + +process.on('SIGTERM', () => gracefulShutdown('SIGTERM')); +process.on('SIGINT', () => gracefulShutdown('SIGINT')); + +// Start server +server.listen(PORT, HOST, () => { + logger.info(`Server running on ${HOST}:${PORT}`); + logger.info(`Environment: ${process.env.NODE_ENV || 'development'}`); +}); + +// Unhandled rejection handler +process.on('unhandledRejection', (reason, promise) => { + logger.error('Unhandled Rejection at:', promise, 'reason:', reason); +}); + +// Uncaught exception handler +process.on('uncaughtException', (error) => { + logger.error('Uncaught Exception:', error); + gracefulShutdown('UNCAUGHT_EXCEPTION'); +}); + +module.exports = server; +``` + +##### Step 1.1.5: Migrate Existing Routes +Migrate all existing route handlers from the monolithic `server.js` to the new route structure. Each route file should: +- Use middleware for authentication +- Implement input validation +- Return standardized response format +- Handle errors consistently + +#### Verification Steps +1. Run existing test suite (if available) +2. Test all authentication flows +3. Verify rate limiting works +4. Check security headers with curl/headers tool +5. Perform penetration testing on key endpoints + +--- + +### 1.2 Implement Database with Encryption at Rest + +**Priority:** Critical +**Estimated Effort:** 60 hours +**Risk Level:** High (data migration required) + +#### Problem Analysis +Current implementation uses in-memory JSON file storage in `chat/.data/` directory: +- `users.json` - User accounts, hashed passwords, sessions +- `affiliates.json` - Affiliate accounts +- `withdrawals.json` - Withdrawal requests +- `feature-requests.json` - Feature request tracking +- `contact-messages.json` - Contact form messages + +**Vulnerabilities:** +- No encryption at rest +- JSON files easily readable if accessed +- No backup encryption +- Tampering risk +- No audit trail + +#### Remediation Steps + +##### Step 1.2.1: Select and Configure Database +```bash +cd chat/ + +# Install PostgreSQL client (or use SQLite for development) +npm install pg@^8.11.3 +npm install sqlite3@^5.1.7 +npm install better-sqlite3@^9.4.3 + +# For production: PostgreSQL with encryption +npm install pg-crypto@^1.1.0 + +# Database migration tool +npm install migrate@^9.2.1 +``` + +##### Step 1.2.2: Create Database Schema with Encryption +```javascript +// src/config/database.js +const { Database } = require('sqlite3'); +const crypto = require('crypto'); + +const dbPath = process.env.DATABASE_PATH || './.data/shopify_ai.db'; + +// Database encryption key (should be stored in HSM or key management service) +const ENCRYPTION_KEY = process.env.DATABASE_ENCRYPTION_KEY; +const IV_LENGTH = 16; +const ALGORITHM = 'aes-256-gcm'; + +function encrypt(text) { + if (!ENCRYPTION_KEY) { + throw new Error('Database encryption key not configured'); + } + + const iv = crypto.randomBytes(IV_LENGTH); + const cipher = crypto.createCipheriv(ALGORITHM, Buffer.from(ENCRYPTION_KEY, 'hex'), iv); + + let encrypted = cipher.update(text, 'utf8', 'hex'); + encrypted += cipher.final('hex'); + + const authTag = cipher.getAuthTag(); + + return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted; +} + +function decrypt(text) { + if (!ENCRYPTION_KEY) { + throw new Error('Database encryption key not configured'); + } + + const parts = text.split(':'); + const iv = Buffer.from(parts[0], 'hex'); + const authTag = Buffer.from(parts[1], 'hex'); + const encrypted = parts[2]; + + const decipher = crypto.createDecipheriv(ALGORITHM, Buffer.from(ENCRYPTION_KEY, 'hex'), iv); + decipher.setAuthTag(authTag); + + let decrypted = decipher.update(encrypted, 'hex', 'utf8'); + decrypted += decipher.final('utf8'); + + return decrypted; +} + +// Initialize database +const db = new Database(dbPath, (err) => { + if (err) { + console.error('Database connection error:', err.message); + } else { + console.log('Connected to SQLite database'); + } +}); + +// Enable WAL mode for better performance +db.pragma('journal_mode = WAL'); + +// Create tables with encrypted fields +db.serialize(() => { + // Users table + db.run(` + CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + email TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + salt TEXT NOT NULL, + name TEXT, + role TEXT DEFAULT 'user', + is_active INTEGER DEFAULT 1, + created_at INTEGER DEFAULT (unixepoch()), + updated_at INTEGER DEFAULT (unixepoch()), + last_login INTEGER, + failed_login_attempts INTEGER DEFAULT 0, + locked_until INTEGER, + two_factor_enabled INTEGER DEFAULT 0, + two_factor_secret TEXT, + password_changed_at INTEGER DEFAULT (unixepoch()) + ) + `); + + // Sessions table + db.run(` + CREATE TABLE IF NOT EXISTS sessions ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + token TEXT NOT NULL, + expires_at INTEGER NOT NULL, + ip_address TEXT, + user_agent TEXT, + created_at INTEGER DEFAULT (unixepoch()), + FOREIGN KEY (user_id) REFERENCES users(id) + ) + `); + + // Refresh tokens table + db.run(` + CREATE TABLE IF NOT EXISTS refresh_tokens ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + token TEXT NOT NULL, + expires_at INTEGER NOT NULL, + created_at INTEGER DEFAULT (unixepoch()), + revoked_at INTEGER, + revoked_reason TEXT, + FOREIGN KEY (user_id) REFERENCES users(id) + ) + `); + + // Password reset tokens table + db.run(` + CREATE TABLE IF NOT EXISTS password_reset_tokens ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + token TEXT NOT NULL, + expires_at INTEGER NOT NULL, + used INTEGER DEFAULT 0, + created_at INTEGER DEFAULT (unixepoch()), + FOREIGN KEY (user_id) REFERENCES users(id) + ) + `); + + // Affiliates table + db.run(` + CREATE TABLE IF NOT EXISTS affiliates ( + id TEXT PRIMARY KEY, + user_id TEXT UNIQUE NOT NULL, + referral_code TEXT UNIQUE NOT NULL, + commission_rate REAL DEFAULT 0.10, + balance REAL DEFAULT 0, + total_earned REAL DEFAULT 0, + payout_method TEXT, + payout_details TEXT, + is_active INTEGER DEFAULT 1, + created_at INTEGER DEFAULT (unixepoch()), + FOREIGN KEY (user_id) REFERENCES users(id) + ) + `); + + // Withdrawals table + db.run(` + CREATE TABLE IF NOT EXISTS withdrawals ( + id TEXT PRIMARY KEY, + affiliate_id TEXT NOT NULL, + amount REAL NOT NULL, + status TEXT DEFAULT 'pending', + payment_method TEXT, + transaction_id TEXT, + notes TEXT, + created_at INTEGER DEFAULT (unixepoch()), + processed_at INTEGER, + FOREIGN KEY (affiliate_id) REFERENCES affiliates(id) + ) + `); + + // Feature requests table + db.run(` + CREATE TABLE IF NOT EXISTS feature_requests ( + id TEXT PRIMARY KEY, + user_id TEXT NOT NULL, + title TEXT NOT NULL, + description TEXT NOT NULL, + status TEXT DEFAULT 'pending', + votes INTEGER DEFAULT 0, + created_at INTEGER DEFAULT (unixepoch()), + updated_at INTEGER DEFAULT (unixepoch()), + FOREIGN KEY (user_id) REFERENCES users(id) + ) + `); + + // Contact messages table + db.run(` + CREATE TABLE IF NOT EXISTS contact_messages ( + id TEXT PRIMARY KEY, + user_id TEXT, + name TEXT NOT NULL, + email TEXT NOT NULL, + subject TEXT, + message TEXT NOT NULL, + status TEXT DEFAULT 'unread', + created_at INTEGER DEFAULT (unixepoch()), + responded_at INTEGER, + responded_by TEXT + ) + `); + + // Audit log table + db.run(` + CREATE TABLE IF NOT EXISTS audit_log ( + id TEXT PRIMARY KEY, + user_id TEXT, + action TEXT NOT NULL, + entity_type TEXT, + entity_id TEXT, + old_values TEXT, + new_values TEXT, + ip_address TEXT, + user_agent TEXT, + created_at INTEGER DEFAULT (unixepoch()) + ) + `); + + // Create indexes + db.run('CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id)'); + db.run('CREATE INDEX IF NOT EXISTS idx_refresh_tokens_token ON refresh_tokens(token)'); + db.run('CREATE INDEX IF NOT EXISTS idx_audit_log_user ON audit_log(user_id)'); + db.run('CREATE INDEX IF NOT EXISTS idx_audit_log_created ON audit_log(created_at)'); +}); + +// Database helper functions +const dbHelpers = { + // Run query with parameters + run(sql, params = []) { + return new Promise((resolve, reject) => { + db.run(sql, params, function(err) { + if (err) reject(err); + else resolve({ id: this.lastID, changes: this.changes }); + }); + }); + }, + + // Get single row + get(sql, params = []) { + return new Promise((resolve, reject) => { + db.get(sql, params, (err, row) => { + if (err) reject(err); + else resolve(row); + }); + }); + }, + + // Get all rows + all(sql, params = []) { + return new Promise((resolve, reject) => { + db.all(sql, params, (err, rows) => { + if (err) reject(err); + else resolve(rows); + }); + }); + }, + + // Insert with auto-generated ID + insert(table, data) { + const columns = Object.keys(data); + const values = Object.values(data); + const placeholders = columns.map(() => '?').join(', '); + + const sql = `INSERT INTO ${table} (${columns.join(', ')}) VALUES (${placeholders})`; + return this.run(sql, values); + }, + + // Update with encrypted fields + update(table, data, where, whereParams = []) { + const updates = Object.keys(data).map(key => `${key} = ?`).join(', '); + const values = [...Object.values(data), ...whereParams]; + + const sql = `UPDATE ${table} SET ${updates} WHERE ${where}`; + return this.run(sql, values); + } +}; + +module.exports = { db, dbHelpers, encrypt, decrypt }; +``` + +##### Step 1.2.3: Data Migration Script +```javascript +// scripts/migrate-data.js +const fs = require('fs'); +const path = require('path'); +const crypto = require('crypto'); +const { db, encrypt } = require('../src/config/database'); + +const DATA_DIR = path.join(__dirname, '../.data'); +const BACKUP_DIR = path.join(__dirname, '../.data_backup_' + Date.now()); + +async function migrate() { + console.log('Starting data migration...'); + + // Create backup directory + fs.mkdirSync(BACKUP_DIR); + + // Migrate each data file + const dataFiles = [ + 'users.json', + 'affiliates.json', + 'withdrawals.json', + 'feature-requests.json', + 'contact-messages.json' + ]; + + for (const file of dataFiles) { + const filePath = path.join(DATA_DIR, file); + + if (fs.existsSync(filePath)) { + console.log(`Migrating ${file}...`); + + // Backup original + fs.copyFileSync(filePath, path.join(BACKUP_DIR, file)); + + // Read and parse + const data = JSON.parse(fs.readFileSync(filePath, 'utf8')); + + // Migrate to database + await migrateFile(file.replace('.json', ''), data); + + console.log(`✓ ${file} migrated successfully`); + } + } + + console.log('Migration complete. Backup created in:', BACKUP_DIR); +} + +async function migrateFile(tableName, dataArray) { + for (const item of dataArray) { + const id = item.id || crypto.randomUUID(); + const columns = ['id']; + const placeholders = ['?']; + const values = [id]; + + for (const [key, value] of Object.entries(item)) { + if (key !== 'id') { + columns.push(key); + placeholders.push('?'); + // Encrypt sensitive fields + if (['password', 'password_hash', 'salt', 'two_factor_secret', 'payout_details'].includes(key)) { + values.push(encrypt(String(value))); + } else { + values.push(typeof value === 'object' ? JSON.stringify(value) : value); + } + } + } + + const sql = `INSERT INTO ${tableName} (${columns.join(', ')}) VALUES (${placeholders.join(', ')})`; + await db.run(sql, values); + } +} + +// Run migration +migrate().catch(console.error); +``` + +##### Step 1.2.4: Implement Audit Trail +```javascript +// src/middleware/auditLogger.js +const { db, dbHelpers } = require('../config/database'); +const crypto = require('crypto'); + +async function auditLog(req, action, entityType, entityId, oldValues = null, newValues = null) { + const auditEntry = { + id: crypto.randomUUID(), + user_id: req.user?.id || null, + action, + entity_type: entityType, + entity_id: entityId, + old_values: oldValues ? JSON.stringify(oldValues) : null, + new_values: newValues ? JSON.stringify(newValues) : null, + ip_address: req.ip || req.headers['x-forwarded-for'], + user_agent: req.headers['user-agent'] + }; + + await dbHelpers.insert('audit_log', auditEntry); +} + +// Middleware for automatic audit logging +function auditMiddleware(action, entityType) { + return async (req, res, next) => { + const originalSend = res.send; + let responseBody; + + res.send = function(body) { + responseBody = body; + return originalSend.apply(this, arguments); + }; + + res.on('finish', async () => { + if (res.statusCode >= 200 && res.statusCode < 300) { + try { + await auditLog(req, action, entityType, req.params.id, null, JSON.parse(responseBody)); + } catch (e) { + console.error('Audit log error:', e); + } + } + }); + + next(); + }; +} + +module.exports = { auditLog, auditMiddleware }; +``` + +#### Verification Steps +1. Verify all data migrated correctly +2. Test encryption/decryption operations +3. Perform database integrity checks +4. Test backup and restore procedures +5. Verify audit logging captures all actions + +--- + +### 1.3 Implement Session Revocation and Token Management + +**Priority:** Critical +**Estimated Effort:** 20 hours +**Risk Level:** Medium + +#### Problem Analysis +Current implementation lacks: +- Session revocation mechanism +- Token blacklisting +- Session enumeration protection +- Device fingerprinting + +#### Remediation Steps + +##### Step 1.3.1: Implement JWT Token Manager +```javascript +// src/utils/tokenManager.js +const crypto = require('crypto'); +const jwt = require('jsonwebtoken'); +const { db, dbHelpers } = require('../config/database'); + +const JWT_SECRET = process.env.JWT_SECRET || process.env.USER_SESSION_SECRET; +const ACCESS_TOKEN_EXPIRY = '15m'; +const REFRESH_TOKEN_EXPIRY = '7d'; +const RESET_TOKEN_EXPIRY = '1h'; + +class TokenManager { + // Generate access token + generateAccessToken(user) { + return jwt.sign( + { + id: user.id, + email: user.email, + role: user.role, + type: 'access' + }, + JWT_SECRET, + { expiresIn: ACCESS_TOKEN_EXPIRY } + ); + } + + // Generate refresh token + async generateRefreshToken(user, req) { + const token = crypto.randomBytes(64).toString('hex'); + const expiresAt = Date.now() + (7 * 24 * 60 * 60 * 1000); // 7 days + + await dbHelpers.insert('refresh_tokens', { + id: crypto.randomUUID(), + user_id: user.id, + token: await this.hashToken(token), + expires_at: expiresAt, + ip_address: req.ip || req.headers['x-forwarded-for'], + user_agent: req.headers['user-agent'] + }); + + return { token, expiresAt }; + } + + // Hash token for storage + async hashToken(token) { + const salt = await crypto.randomBytes(16).toString('hex'); + const hash = await crypto.pbkdf2Async(token, salt, 100000, 64, 'sha512'); + return salt + ':' + hash.toString('hex'); + } + + // Verify refresh token + async verifyRefreshToken(token, userId) { + const hashedToken = await this.hashToken(token); + const storedToken = await dbHelpers.get( + 'SELECT * FROM refresh_tokens WHERE user_id = ? AND token = ? AND used = 0 AND expires_at > ?', + [userId, hashedToken, Date.now()] + ); + + return storedToken || null; + } + + // Revoke refresh token + async revokeRefreshToken(token, userId, reason = 'user_logout') { + const hashedToken = await this.hashToken(token); + await dbHelpers.update( + 'refresh_tokens', + { used: 1, revoked_at: Date.now(), revoked_reason: reason }, + 'user_id = ? AND token = ?', + [userId, hashedToken] + ); + } + + // Revoke all user tokens + async revokeAllUserTokens(userId, reason = 'security_reset') { + await dbHelpers.run( + 'UPDATE refresh_tokens SET used = 1, revoked_at = ?, revoked_reason = ? WHERE user_id = ? AND used = 0', + [Date.now(), reason, userId] + ); + } + + // Generate password reset token + async generatePasswordResetToken(user) { + const token = crypto.randomBytes(32).toString('hex'); + const expiresAt = Date.now() + (60 * 60 * 1000); // 1 hour + + // Invalidate old tokens + await dbHelpers.update( + 'password_reset_tokens', + { used: 1 }, + 'user_id = ? AND used = 0', + [user.id] + ); + + await dbHelpers.insert('password_reset_tokens', { + id: crypto.randomUUID(), + user_id: user.id, + token: await this.hashToken(token), + expires_at: expiresAt + }); + + return { token, expiresAt }; + } + + // Verify and use password reset token + async verifyAndUsePasswordResetToken(token, userId) { + const hashedToken = await this.hashToken(token); + const resetToken = await dbHelpers.get( + 'SELECT * FROM password_reset_tokens WHERE user_id = ? AND token = ? AND used = 0 AND expires_at > ?', + [userId, hashedToken, Date.now()] + ); + + if (resetToken) { + await dbHelpers.update( + 'password_reset_tokens', + { used: 1 }, + 'id = ?', + [resetToken.id] + ); + return true; + } + + return false; + } +} + +module.exports = new TokenManager(); +``` + +##### Step 1.3.2: Implement Session Fingerprinting +```javascript +// src/middleware/sessionFingerprint.js +const crypto = require('crypto'); + +function generateFingerprint(req) { + const components = [ + req.headers['user-agent'] || '', + req.headers['accept-language'] || '', + req.headers['accept-encoding'] || '', + req.ip || '', + req.headers['x-forwarded-for'] || '' + ]; + + return crypto + .createHash('sha256') + .update(components.join('|')) + .digest('hex') + .substring(0, 32); +} + +function validateFingerprint(req, storedFingerprint) { + const currentFingerprint = generateFingerprint(req); + return crypto.timingSafeEqual( + Buffer.from(currentFingerprint), + Buffer.from(storedFingerprint) + ); +} + +module.exports = { generateFingerprint, validateFingerprint }; +``` + +#### Verification Steps +1. Test token generation and verification +2. Verify token revocation works +3. Test session fingerprinting +4. Check token expiry handling + +--- + +## Phase 2: Authentication Security Enhancement + +### 2.1 Strengthen Password Authentication + +**Priority:** High +**Estimated Effort:** 16 hours +**Risk Level:** Low + +#### Problem Analysis +Current password security: +- bcrypt with 12 salt rounds ✓ +- Password policy (12+ chars, complexity) ✓ +- Account lockout (5 attempts/15 min) ✓ + +**Improvements needed:** +- Password strength meter +- Breach detection (HaveIBeenPwned API) +- Password history enforcement +- Progressive delays on failed attempts + +#### Remediation Steps + +##### Step 2.1.1: Enhanced Password Validator +```javascript +// src/utils/passwordValidator.js +const bcrypt = require('bcrypt'); +const crypto = require('crypto'); + +const SALT_ROUNDS = 12; + +class PasswordValidator { + constructor() { + this.bannedPasswords = new Set([ + 'password', '123456', '12345678', 'qwerty', 'abc123', + 'password123', 'admin123', 'letmein', 'welcome' + ]); + } + + validate(password, email = '') { + const errors = []; + + // Length check + if (password.length < 12) { + errors.push('Password must be at least 12 characters long'); + } + + // Complexity requirements + if (!/[A-Z]/.test(password)) { + errors.push('Password must contain at least one uppercase letter'); + } + if (!/[a-z]/.test(password)) { + errors.push('Password must contain at least one lowercase letter'); + } + if (!/[0-9]/.test(password)) { + errors.push('Password must contain at least one number'); + } + if (!/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) { + errors.push('Password must contain at least one special character'); + } + + // Common password check + if (this.bannedPasswords.has(password.toLowerCase())) { + errors.push('Password is too common'); + } + + // Email-based password check + const emailPart = email.split('@')[0]; + if (emailPart && password.toLowerCase().includes(emailPart.toLowerCase())) { + errors.push('Password cannot contain your email username'); + } + + // Sequential character check + if (this.hasSequentialChars(password)) { + errors.push('Password cannot contain more than 3 sequential characters'); + } + + return { + isValid: errors.length === 0, + errors + }; + } + + hasSequentialChars(password) { + const lower = password.toLowerCase(); + for (let i = 0; i < lower.length - 2; i++) { + const c1 = lower.charCodeAt(i); + const c2 = lower.charCodeAt(i + 1); + const c3 = lower.charCodeAt(i + 2); + if (c2 === c1 + 1 && c3 === c1 + 2) { + return true; + } + } + return false; + } + + async hash(password) { + return bcrypt.hash(password, SALT_ROUNDS); + } + + async compare(password, hash) { + return bcrypt.compare(password, hash); + } + + async checkBreachedPassword(password) { + // Hash password with SHA-1 + const sha1Hash = crypto + .createHash('sha1') + .update(password) + .digest('hex') + .toUpperCase(); + + const prefix = sha1Hash.substring(0, 5); + const suffix = sha1Hash.substring(5); + + try { + const response = await fetch( + `https://api.pwnedpasswords.com/range/${prefix}`, + { headers: { 'Add-Padding': 'true' } } + ); + + const text = await response.text(); + const lines = text.split('\n'); + + for (const line of lines) { + const [hashSuffix, count] = line.split(':'); + if (hashSuffix.trim() === suffix) { + return { pwned: true, count: parseInt(count, 10) }; + } + } + + return { pwned: false, count: 0 }; + } catch (error) { + console.error('Breach check failed:', error); + return { error: true }; + } + } +} + +module.exports = new PasswordValidator(); +``` + +##### Step 2.1.2: Progressive Account Lockout +```javascript +// src/middleware/progressiveLockout.js +const { db, dbHelpers } = require('../config/database'); +const crypto = require('crypto'); + +class ProgressiveLockout { + getLockoutDuration(failedAttempts) { + if (failedAttempts >= 10) return 24 * 60 * 60 * 1000; // 24 hours + if (failedAttempts >= 7) return 12 * 60 * 60 * 1000; // 12 hours + if (failedAttempts >= 5) return 60 * 60 * 1000; // 1 hour + if (failedAttempts >= 3) return 15 * 60 * 1000; // 15 minutes + return 5 * 60 * 1000; // 5 minutes + } + + async checkLockout(email) { + const user = await dbHelpers.get( + 'SELECT id, email, failed_login_attempts, locked_until FROM users WHERE email = ? AND is_active = 1', + [email] + ); + + if (!user) { + // Generic error message to prevent enumeration + return { locked: false, error: 'Invalid credentials' }; + } + + if (user.locked_until && user.locked_until > Date.now()) { + const remainingTime = Math.ceil((user.locked_until - Date.now()) / 60000); + return { + locked: true, + error: `Account locked. Try again in ${remainingTime} minutes.`, + retryAfter: remainingTime * 60 + }; + } + + return { locked: false, user }; + } + + async recordFailedAttempt(userId) { + const user = await dbHelpers.get( + 'SELECT failed_login_attempts, locked_until FROM users WHERE id = ?', + [userId] + ); + + const failedAttempts = (user?.failed_login_attempts || 0) + 1; + const lockoutDuration = this.getLockoutDuration(failedAttempts); + + let lockedUntil = null; + if (failedAttempts >= 5) { + lockedUntil = Date.now() + lockoutDuration; + } + + await dbHelpers.update( + 'users', + { + failed_login_attempts: failedAttempts, + locked_until: lockedUntil + }, + 'id = ?', + [userId] + ); + + return { failedAttempts, locked: lockedUntil !== null }; + } + + async resetFailedAttempts(userId) { + await dbHelpers.update( + 'users', + { failed_login_attempts: 0, locked_until: null }, + 'id = ?', + [userId] + ); + } +} + +module.exports = new ProgressiveLockout(); +``` + +#### Verification Steps +1. Test all password validation rules +2. Verify breach detection integration +3. Test progressive lockout timing +4. Check generic error messages prevent enumeration + +--- + +### 2.2 Implement Two-Factor Authentication + +**Priority:** High +**Estimated Effort:** 24 hours +**Risk Level:** Medium + +#### Remediation Steps + +##### Step 2.2.1: TOTP Implementation +```javascript +// src/utils/totp.js +const crypto = require('crypto'); +const base32 = require('hi-base32'); + +const DIGITS = 6; +const PERIOD = 30; +const ALGORITHM = 'sha1'; + +class TOTP { + generateSecret(length = 20) { + return crypto.randomBytes(length).toString('hex'); + } + + generateSecretBase32() { + const buffer = crypto.randomBytes(10); + return base32.encode(buffer).replace(/=/g, ''); + } + + getotpauthURL(secret, issuer, account, label) { + const encodedIssuer = encodeURIComponent(issuer); + const encodedAccount = encodeURIComponent(account); + const encodedSecret = secret.replace(/ /g, ''); + + return `otpauth://totp/${encodedIssuer}:${encodedAccount}?secret=${encodedSecret}&issuer=${encodedIssuer}&algorithm=${ALGORITHM}&digits=${DIGITS}&period=${PERIOD}`; + } + + verify(token, secret, window = 1) { + const epoch = Math.floor(Date.now() / 1000); + const timeStep = Math.floor(epoch / PERIOD); + + for (let i = -window; i <= window; i++) { + const time = timeStep + i; + const generatedToken = this.generateToken(secret, time); + + if (this.timingSafeEqual(token, generatedToken)) { + return { valid: true, delta: i }; + } + } + + return { valid: false }; + } + + generateToken(secret, time) { + const buffer = Buffer.alloc(8); + buffer.writeBigUInt64BE(BigInt(time), 0); + + const decodedSecret = base32.decode(secret.replace(/ /g, '')); + const hmac = crypto.createHmac(ALGORITHM, decodedSecret); + hmac.update(buffer); + const hash = hmac.digest(); + + const offset = hash[hash.length - 1] & 0xf; + const truncatedHash = hash.readUInt32BE(offset) & 0x7fffffff; + const token = truncatedHash % Math.pow(10, DIGITS); + + return token.toString().padStart(DIGITS, '0'); + } + + timingSafeEqual(a, b) { + if (a.length !== b.length) return false; + return crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b)); + } +} + +module.exports = new TOTP(); +``` + +##### Step 2.2.2: Two-Factor Authentication Routes +```javascript +// src/routes/twoFactor.js +const express = require('express'); +const router = express.Router(); +const { db, dbHelpers } = require('../config/database'); +const crypto = require('crypto'); +const totp = require('../utils/totp'); +const passwordValidator = require('../utils/passwordValidator'); +const { requireUserAuth } = require('../middleware/auth'); + +// Generate 2FA setup +router.post('/setup', requireUserAuth, async (req, res) => { + try { + const user = await dbHelpers.get('SELECT * FROM users WHERE id = ?', [req.user.id]); + + // Generate new secret + const secret = totp.generateSecretBase32(); + + // Generate recovery codes + const recoveryCodes = Array(10).fill(0).map(() => + crypto.randomBytes(4).toString('hex').toUpperCase() + ); + + // Store temporarily (not yet enabled) + await dbHelpers.update( + 'users', + { + two_factor_secret: passwordValidator.hash(secret), // Store encrypted + two_factor_temp_secret: secret, + two_factor_recovery_codes: await passwordValidator.hash(JSON.stringify(recoveryCodes)) + }, + 'id = ?', + [req.user.id] + ); + + // Generate QR code URL + const otpauthURL = totp.getotpauthURL( + secret, + 'Shopify AI', + user.email, + `${user.name || user.email} (Shopify AI)` + ); + + res.json({ + success: true, + secret, + otpauthURL, + recoveryCodes // Show only once + }); + } catch (error) { + res.status(500).json({ error: 'Failed to setup 2FA' }); + } +}); + +// Verify and enable 2FA +router.post('/enable', requireUserAuth, async (req, res) => { + try { + const { token } = req.body; + const user = await dbHelpers.get('SELECT two_factor_temp_secret FROM users WHERE id = ?', [req.user.id]); + + if (!user?.two_factor_temp_secret) { + return res.status(400).json({ error: '2FA setup not initiated' }); + } + + const result = totp.verify(token, user.two_factor_temp_secret); + + if (!result.valid) { + return res.status(400).json({ error: 'Invalid verification code' }); + } + + // Enable 2FA + await dbHelpers.update( + 'users', + { + two_factor_secret: user.two_factor_temp_secret, + two_factor_temp_secret: null, + two_factor_enabled: 1 + }, + 'id = ?', + [req.user.id] + ); + + res.json({ success: true, message: '2FA enabled successfully' }); + } catch (error) { + res.status(500).json({ error: 'Failed to enable 2FA' }); + } +}); + +// Verify 2FA during login +router.post('/verify', async (req, res) => { + try { + const { email, password, token } = req.body; + + // First verify password + const user = await dbHelpers.get( + 'SELECT * FROM users WHERE email = ? AND is_active = 1', + [email] + ); + + if (!user || !(await passwordValidator.compare(password, user.password_hash))) { + return res.status(401).json({ error: 'Invalid credentials' }); + } + + if (!user.two_factor_enabled) { + return res.status(400).json({ error: '2FA not enabled for this account' }); + } + + const result = totp.verify(token, user.two_factor_secret); + + if (!result.valid) { + return res.status(401).json({ error: 'Invalid 2FA code' }); + } + + // Generate session + const tokenManager = require('../utils/tokenManager'); + const accessToken = tokenManager.generateAccessToken(user); + const refreshToken = await tokenManager.generateRefreshToken(user, req); + + res.json({ + success: true, + accessToken, + refreshToken: refreshToken.token, + expiresAt: refreshToken.expiresAt + }); + } catch (error) { + res.status(500).json({ error: '2FA verification failed' }); + } +}); + +module.exports = router; +``` + +#### Verification Steps +1. Test TOTP generation and verification +2. Verify QR code scanning works +3. Test recovery codes +4. Test backup codes + +--- + +## Phase 3: Input Validation & Sanitization Enhancement + +### 3.1 Comprehensive Input Sanitization Framework + +**Priority:** High +**Estimated Effort:** 20 hours +**Risk Level:** Medium + +#### Remediation Steps + +##### Step 3.1.1: Create Comprehensive Sanitizer +```javascript +// src/utils/sanitizer.js +class Sanitizer { + // HTML sanitization + sanitizeHTML(input) { + if (typeof input !== 'string') return input; + + return input + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(/\//g, '/'); + } + + // XSS prevention for user-generated content + sanitizeUserContent(input) { + if (typeof input !== 'string') return input; + + return input + .replace(/)<[^<]*)*<\/script>/gi, '') + .replace(/javascript:/gi, '') + .replace(/on\w+=/gi, '') + .replace(/