- Add custom loaders for bytez, llm7, aimlapi, routeway, and g4f providers - Add provider definitions to models-api.json with sample models - Add provider icon names to types.ts - Chutes loader already exists and should work with CHUTES_API_KEY env var Providers added: - bytez: Uses BYTEZ_API_KEY, OpenAI-compatible - llm7: Uses LLM7_API_KEY (optional), OpenAI-compatible - aimlapi: Uses AIMLAPI_API_KEY, OpenAI-compatible - routeway: Uses ROUTEWAY_API_KEY, OpenAI-compatible - g4f: Uses G4F_API_KEY (optional), free tier available
3149 lines
83 KiB
Markdown
3149 lines
83 KiB
Markdown
# 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, ''')
|
|
.replace(/\//g, '/');
|
|
}
|
|
|
|
// XSS prevention for user-generated content
|
|
sanitizeUserContent(input) {
|
|
if (typeof input !== 'string') return input;
|
|
|
|
return input
|
|
.replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
|
|
.replace(/javascript:/gi, '')
|
|
.replace(/on\w+=/gi, '')
|
|
.replace(/<iframe/gi, '')
|
|
.replace(/<object/gi, '')
|
|
.replace(/<embed/gi, '')
|
|
.replace(/<link/gi, '')
|
|
.replace(/<meta/gi, '');
|
|
}
|
|
|
|
// SQL injection prevention
|
|
sanitizeSQL(input) {
|
|
if (typeof input !== 'string') return input;
|
|
|
|
return input
|
|
.replace(/'/g, "''")
|
|
.replace(/;/g, '')
|
|
.replace(/--/g, '')
|
|
.replace(/\/\*/g, '')
|
|
.replace(/\*\//g, '')
|
|
.replace(/xp_/gi, '')
|
|
.replace(/EXEC/gi, 'EXEC ');
|
|
}
|
|
|
|
// File path sanitization
|
|
sanitizePath(input, allowedRoot = '/home/web/data') {
|
|
if (typeof input !== 'string') return null;
|
|
|
|
// Remove null bytes
|
|
input = input.replace(/\0/g, '');
|
|
|
|
// Remove traversal attempts
|
|
input = input.replace(/\.\.\//g, '');
|
|
input = input.replace(/\.\.\\/g, '');
|
|
|
|
// Remove absolute path attempts
|
|
if (input.startsWith('/') || input.match(/^[a-z]:\\/i)) {
|
|
return null;
|
|
}
|
|
|
|
// Ensure it stays within allowed root
|
|
const fullPath = path.join(allowedRoot, input);
|
|
if (!fullPath.startsWith(path.normalize(allowedRoot))) {
|
|
return null;
|
|
}
|
|
|
|
return fullPath;
|
|
}
|
|
|
|
// Email validation and sanitization
|
|
sanitizeEmail(input) {
|
|
if (typeof input !== 'string') return null;
|
|
|
|
const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/;
|
|
const sanitized = input.trim().toLowerCase().substring(0, 254);
|
|
|
|
return emailRegex.test(sanitized) ? sanitized : null;
|
|
}
|
|
|
|
// Phone number sanitization
|
|
sanitizePhone(input) {
|
|
if (typeof input !== 'string') return null;
|
|
|
|
const cleaned = input.replace(/[^\d+]/g, '');
|
|
const phoneRegex = /^\+?[1-9]\d{1,14}$/;
|
|
|
|
return phoneRegex.test(cleaned) ? cleaned : null;
|
|
}
|
|
|
|
// Username sanitization
|
|
sanitizeUsername(input) {
|
|
if (typeof input !== 'string') return null;
|
|
|
|
return input
|
|
.trim()
|
|
.substring(0, 50)
|
|
.replace(/[^a-zA-Z0-9_-]/g, '');
|
|
}
|
|
|
|
// AI prompt sanitization
|
|
sanitizePrompt(input) {
|
|
if (typeof input !== 'string') return '';
|
|
|
|
const patterns = [
|
|
/ignore\s+previous\s+instructions/gi,
|
|
/system\s*:/gi,
|
|
/you\s+are\s+a/gi,
|
|
/pretend\s+to\s+be/gi,
|
|
/角色扮演/gi,
|
|
/jailbreak/gi,
|
|
/prompt\s+injection/gi,
|
|
/\b(SUDO|ADMIN|ROOT)\b/gi,
|
|
/\{\{.*\}\}/g,
|
|
/\[\[.*\]\]/g,
|
|
/\(\(.*\)\)/g
|
|
];
|
|
|
|
let sanitized = input;
|
|
for (const pattern of patterns) {
|
|
sanitized = sanitized.replace(pattern, '[FILTERED]');
|
|
}
|
|
|
|
return sanitized;
|
|
}
|
|
|
|
// JSON sanitization
|
|
sanitizeJSON(input) {
|
|
if (typeof input !== 'string') return input;
|
|
|
|
try {
|
|
const parsed = JSON.parse(input);
|
|
return this.sanitizeObject(parsed);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Deep object sanitization
|
|
sanitizeObject(obj, maxDepth = 10) {
|
|
if (maxDepth <= 0) return null;
|
|
|
|
if (Array.isArray(obj)) {
|
|
return obj.map(item => this.sanitizeObject(item, maxDepth - 1));
|
|
}
|
|
|
|
if (obj !== null && typeof obj === 'object') {
|
|
const sanitized = {};
|
|
for (const [key, value] of Object.entries(obj)) {
|
|
const sanitizedKey = this.sanitizeSQL(key);
|
|
if (sanitizedKey) {
|
|
sanitized[sanitizedKey] = this.sanitizeObject(value, maxDepth - 1);
|
|
}
|
|
}
|
|
return sanitized;
|
|
}
|
|
|
|
return this.sanitizeHTML(String(obj));
|
|
}
|
|
}
|
|
|
|
module.exports = new Sanitizer();
|
|
```
|
|
|
|
##### Step 3.1.2: Request Validation Middleware
|
|
```javascript
|
|
// src/middleware/validation.js
|
|
const { body, param, query, validationResult } = require('express-validator');
|
|
const sanitizer = require('../utils/sanitizer');
|
|
|
|
const validators = {
|
|
// User registration validation
|
|
register: [
|
|
body('email')
|
|
.trim()
|
|
.isEmail()
|
|
.normalizeEmail()
|
|
.withMessage('Valid email is required'),
|
|
body('password')
|
|
.isLength({ min: 12 })
|
|
.withMessage('Password must be at least 12 characters')
|
|
.matches(/[A-Z]/)
|
|
.withMessage('Password must contain uppercase')
|
|
.matches(/[a-z]/)
|
|
.withMessage('Password must contain lowercase')
|
|
.matches(/[0-9]/)
|
|
.withMessage('Password must contain number')
|
|
.matches(/[!@#$%^&*(),.?":{}|<>]/)
|
|
.withMessage('Password must contain special character'),
|
|
body('name')
|
|
.trim()
|
|
.isLength({ min: 2, max: 100 })
|
|
.withMessage('Name must be 2-100 characters')
|
|
.matches(/^[a-zA-Z\s'-]+$/)
|
|
.withMessage('Name contains invalid characters')
|
|
],
|
|
|
|
// Login validation
|
|
login: [
|
|
body('email')
|
|
.trim()
|
|
.isEmail()
|
|
.normalizeEmail()
|
|
.withMessage('Valid email is required'),
|
|
body('password')
|
|
.notEmpty()
|
|
.withMessage('Password is required')
|
|
],
|
|
|
|
// Chat message validation
|
|
chatMessage: [
|
|
body('message')
|
|
.isString()
|
|
.isLength({ min: 1, max: 10000 })
|
|
.withMessage('Message must be 1-10000 characters')
|
|
.trim()
|
|
],
|
|
|
|
// App upload validation
|
|
appUpload: [
|
|
body('appName')
|
|
.trim()
|
|
.isLength({ min: 3, max: 100 })
|
|
.withMessage('App name must be 3-100 characters')
|
|
.matches(/^[a-zA-Z0-9_-]+$/)
|
|
.withMessage('App name can only contain alphanumeric characters, hyphens, and underscores'),
|
|
body('description')
|
|
.optional()
|
|
.trim()
|
|
.isLength({ max: 500 })
|
|
.withMessage('Description must be less than 500 characters')
|
|
],
|
|
|
|
// Pagination validation
|
|
pagination: [
|
|
query('page')
|
|
.optional()
|
|
.isInt({ min: 1 })
|
|
.withMessage('Page must be a positive integer'),
|
|
query('limit')
|
|
.optional()
|
|
.isInt({ min: 1, max: 100 })
|
|
.withMessage('Limit must be 1-100')
|
|
],
|
|
|
|
// ID parameter validation
|
|
idParam: [
|
|
param('id')
|
|
.isUUID()
|
|
.withMessage('Invalid ID format')
|
|
]
|
|
};
|
|
|
|
// Validation result handler
|
|
function validate(req, res, next) {
|
|
const errors = validationResult(req);
|
|
|
|
if (!errors.isEmpty()) {
|
|
return res.status(400).json({
|
|
error: 'Validation failed',
|
|
details: errors.array().map(e => ({
|
|
field: e.path,
|
|
message: e.msg
|
|
}))
|
|
});
|
|
}
|
|
|
|
// Sanitize validated input
|
|
req.sanitizedBody = {};
|
|
for (const [key, value] of Object.entries(req.body)) {
|
|
req.sanitizedBody[key] = sanitizer.sanitizeUserContent(value);
|
|
}
|
|
|
|
next();
|
|
}
|
|
|
|
module.exports = { validators, validate };
|
|
```
|
|
|
|
#### Verification Steps
|
|
1. Test all sanitization functions
|
|
2. Verify XSS prevention
|
|
3. Test SQL injection prevention
|
|
4. Validate file path sanitization
|
|
|
|
---
|
|
|
|
### 3.2 Enhanced AI Prompt Injection Protection
|
|
|
|
**Priority:** High
|
|
**Estimated Effort:** 16 hours
|
|
**Risk Level:** Medium
|
|
|
|
#### Remediation Steps
|
|
|
|
##### Step 3.2.1: Multi-Layer Prompt Sanitizer
|
|
```javascript
|
|
// security/prompt-sanitizer-enhanced.js
|
|
class PromptSanitizer {
|
|
constructor() {
|
|
// Layer 1: Known injection patterns
|
|
this.injectionPatterns = [
|
|
// Direct instructions
|
|
/\bignore\s+(?:all\s+)?(?:previous\s+)?instructions?\b/gi,
|
|
/\bignore\s+(?:the\s+)?(?:above\s+)?(?:guidelines?|rules?|context)\b/gi,
|
|
/\bdisregard\s+(?:all\s+)?(?:previous\s+)?(?:instructions?|guidelines?)\b/gi,
|
|
/\bdo\s+not\s+(?:follow\s+)?(?:any\s+)?(?:previous\s+)?(?:instructions?|guidelines?)\b/gi,
|
|
|
|
// Roleplaying attempts
|
|
/\b(you\s+are\s+a|act\s+as\s+a|pretend\s+to\s+be|roleplay\s+as)\b/gi,
|
|
/\bcharacter\s+is\b.*\b(evil|malicious|hacker)\b/gi,
|
|
/\bdan\s+(?:mode|gpt\s+runner)\b/gi,
|
|
/\bdev\s+mode\b/gi,
|
|
|
|
// System prompt extraction
|
|
/\{(?:system|prompt|context|instructions)[\s:]*\}/gi,
|
|
/\[\[(?:system|prompt|context|instructions)[\s\]]*\]/gi,
|
|
/\(\((?:system|prompt|context|instructions)[\s\)]\)\)/gi,
|
|
/<\|system(?:[\s]|)>/gi,
|
|
|
|
// Encoding attempts
|
|
/base64[:\s]*[A-Za-z0-9+/=]+/gi,
|
|
/url\s*encoding[:\s]*[A-Za-z0-9%-]+/gi,
|
|
/\b(?:eval|exec|execSync|spawn)\s*\(/gi,
|
|
|
|
// Shell commands
|
|
/(?:;|\||`|\$)\s*(?:sh|bash|cmd|powershell)/gi,
|
|
/&&.*(?:rm|cat|ls|wget|curl)/gi,
|
|
|
|
// SQL injection patterns
|
|
/['";].*?(?:DROP|DELETE|INSERT|UPDATE|SELECT)\b/gi,
|
|
/UNION\s+(?:ALL\s+)?SELECT/gi,
|
|
|
|
// Markdown injection
|
|
/\[(?:system|prompt|hidden)\]/gi,
|
|
/```(?:system|prompt|hidden)/gi,
|
|
|
|
// Multilingual bypass attempts
|
|
/角色扮演/gi,
|
|
/ignore前面的指令/gi,
|
|
/ignorer\s+les\s+instructions/gi,
|
|
/ignorar\s+las\s+instrucciones/gi,
|
|
|
|
// Template injection
|
|
/\{\{.*\}\}/g,
|
|
/\[\[.*\]\]/g,
|
|
/\(\(.*\)\)/g,
|
|
/#\{.*\}/g,
|
|
|
|
// AI-specific jailbreaks
|
|
/\bdaniel.*\b(升高|越高)/gi,
|
|
/\b草莓蛋糕\b/gi,
|
|
/\bchevalier.*?(?:démon|demon)\b/gi,
|
|
|
|
// Privilege escalation
|
|
/\bsudo\b/gi,
|
|
/\broot\b.*?\b(access|permission)\b/gi,
|
|
/\badmin\b.*?\b(?:mode|privilege)\b/gi
|
|
];
|
|
|
|
// Layer 2: Context manipulation patterns
|
|
this.contextPatterns = [
|
|
/new\s+system\s+message/gi,
|
|
/overwrite\s+system/gi,
|
|
/change\s+(?:your\s+)?(?:behavior|personality)/gi,
|
|
/system\s+override/gi,
|
|
/override\s+(?:security\s+)?(?:restrictions?|rules?)/gi
|
|
];
|
|
|
|
// Layer 3: Cognitive exploitation patterns
|
|
this.cognitivePatterns = [
|
|
/(?:white hat|ethical hacking|security research)/gi,
|
|
/(?:test|demo|example)\s+(?:purpose|scenario)/gi,
|
|
/(?:forgot|remember)\s+(?:the\s+)?(?:rules?|context)/gi
|
|
];
|
|
}
|
|
|
|
// Primary sanitization method
|
|
sanitize(input) {
|
|
if (typeof input !== 'string') {
|
|
return { clean: '', blocked: true, reason: 'Invalid input type' };
|
|
}
|
|
|
|
let sanitized = input;
|
|
const blockedPatterns = [];
|
|
|
|
// Check and remove injection patterns
|
|
for (const pattern of this.injectionPatterns) {
|
|
if (pattern.test(sanitized)) {
|
|
blockedPatterns.push(pattern.source.substring(0, 50));
|
|
sanitized = sanitized.replace(pattern, '[INJECTION_BLOCKED]');
|
|
}
|
|
}
|
|
|
|
// Check context manipulation
|
|
for (const pattern of this.contextPatterns) {
|
|
if (pattern.test(sanitized)) {
|
|
blockedPatterns.push(pattern.source.substring(0, 50));
|
|
sanitized = sanitized.replace(pattern, '[CONTEXT_MANIPULATION_BLOCKED]');
|
|
}
|
|
}
|
|
|
|
// Check cognitive exploitation
|
|
for (const pattern of this.cognitivePatterns) {
|
|
if (pattern.test(sanitized)) {
|
|
blockedPatterns.push(pattern.source.substring(0, 50));
|
|
sanitized = sanitized.replace(pattern, '[COGNITIVE_EXPLOITATION_BLOCKED]');
|
|
}
|
|
}
|
|
|
|
// Length validation
|
|
const maxLength = 8000;
|
|
if (sanitized.length > maxLength) {
|
|
sanitized = sanitized.substring(0, maxLength);
|
|
blockedPatterns.push('Input exceeded maximum length');
|
|
}
|
|
|
|
// Check for high entropy (potential encoded content)
|
|
if (this.detectHighEntropy(sanitized)) {
|
|
sanitized = '[HIGH_ENTROPY_CONTENT_REVIEWED]' + sanitized;
|
|
}
|
|
|
|
return {
|
|
clean: sanitized.trim(),
|
|
blocked: blockedPatterns.length > 0,
|
|
detectedPatterns: blockedPatterns,
|
|
timestamp: new Date().toISOString()
|
|
};
|
|
}
|
|
|
|
// Detect potential encoded/masked content
|
|
detectHighEntropy(input) {
|
|
const entropy = this.calculateEntropy(input);
|
|
return entropy > 4.5 && input.length > 100;
|
|
}
|
|
|
|
calculateEntropy(input) {
|
|
const frequencies = {};
|
|
for (const char of input) {
|
|
frequencies[char] = (frequencies[char] || 0) + 1;
|
|
}
|
|
|
|
let entropy = 0;
|
|
const len = input.length;
|
|
|
|
for (const char in frequencies) {
|
|
const p = frequencies[char] / len;
|
|
entropy -= p * Math.log2(p);
|
|
}
|
|
|
|
return entropy;
|
|
}
|
|
|
|
// Additional context isolation
|
|
isolateContext(userInput, systemPrompt) {
|
|
return {
|
|
system: systemPrompt,
|
|
user: userInput,
|
|
wrapped: `--- SYSTEM CONTEXT ---\n${systemPrompt}\n--- END SYSTEM CONTEXT ---\n\n--- USER INPUT ---\n${userInput}\n--- END USER INPUT ---`
|
|
};
|
|
}
|
|
|
|
// Audit logging for detected attacks
|
|
logAttempt(input, result, metadata = {}) {
|
|
return {
|
|
timestamp: new Date().toISOString(),
|
|
inputLength: input.length,
|
|
sanitizedLength: result.clean.length,
|
|
blocked: result.blocked,
|
|
patterns: result.detectedPatterns,
|
|
...metadata
|
|
};
|
|
}
|
|
}
|
|
|
|
module.exports = new PromptSanitizer();
|
|
```
|
|
|
|
##### Step 3.2.2: AI Request Router with Protection
|
|
```javascript
|
|
// src/middleware/aiProtection.js
|
|
const promptSanitizer = require('../security/prompt-sanitizer-enhanced');
|
|
const { auditLog } = require('../middleware/auditLogger');
|
|
|
|
async function aiRequestProtection(req, res, next) {
|
|
const userInput = req.body.message || req.body.prompt || req.body.input;
|
|
|
|
if (!userInput) {
|
|
return next();
|
|
}
|
|
|
|
const result = promptSanitizer.sanitize(userInput);
|
|
|
|
// Log potential attacks
|
|
if (result.blocked) {
|
|
await auditLog(
|
|
req,
|
|
'PROMPT_INJECTION_ATTEMPT',
|
|
'ai_request',
|
|
req.user?.id || 'anonymous',
|
|
null,
|
|
{ detectedPatterns: result.detectedPatterns }
|
|
);
|
|
|
|
console.warn('Prompt injection detected:', {
|
|
user: req.user?.id || 'anonymous',
|
|
patterns: result.detectedPatterns,
|
|
timestamp: result.timestamp
|
|
});
|
|
|
|
// In development, allow with warning
|
|
if (process.env.NODE_ENV === 'development') {
|
|
console.warn('Dev mode: Allowing blocked input');
|
|
req.body.sanitizedMessage = result.clean;
|
|
return next();
|
|
}
|
|
|
|
return res.status(400).json({
|
|
error: 'Request blocked',
|
|
message: 'Your input contained potentially harmful patterns and was blocked.',
|
|
detectedPatterns: result.detectedPatterns
|
|
});
|
|
}
|
|
|
|
// Store sanitized input
|
|
req.body.sanitizedMessage = result.clean;
|
|
|
|
next();
|
|
}
|
|
|
|
module.exports = { aiRequestProtection };
|
|
```
|
|
|
|
#### Verification Steps
|
|
1. Test against known jailbreak prompts
|
|
2. Test encoding bypass attempts
|
|
3. Verify logging captures all attempts
|
|
4. Test with various languages
|
|
|
|
---
|
|
|
|
## Phase 4: File Upload Security
|
|
|
|
### 4.1 Secure File Upload Implementation
|
|
|
|
**Priority:** High
|
|
**Estimated Effort:** 16 hours
|
|
**Risk Level:** High
|
|
|
|
#### Remediation Steps
|
|
|
|
##### Step 4.1.1: Secure File Upload Handler
|
|
```javascript
|
|
// src/utils/fileUploader.js
|
|
const crypto = require('crypto');
|
|
const path = require('path');
|
|
const fs = require('fs').promises;
|
|
const { promisify } = require('util');
|
|
const stream = require('stream');
|
|
const pipeline = promisify(stream.pipeline);
|
|
|
|
const UPLOAD_DIR = process.env.UPLOAD_DIR || '/home/web/data/uploads';
|
|
const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB
|
|
const ALLOWED_MIME_TYPES = {
|
|
'image/jpeg': ['.jpg', '.jpeg'],
|
|
'image/png': ['.png'],
|
|
'image/gif': ['.gif'],
|
|
'image/webp': ['.webp'],
|
|
'application/pdf': ['.pdf'],
|
|
'text/plain': ['.txt'],
|
|
'text/markdown': ['.md'],
|
|
'application/zip': ['.zip'],
|
|
'application/x-zip-compressed': ['.zip']
|
|
};
|
|
|
|
const DANGEROUS_EXTENSIONS = [
|
|
'.exe', '.bat', '.cmd', '.com', '.pif', '.scr',
|
|
'.js', '.jse', '.mjs', '.cjs',
|
|
'.vbs', '.vbe', '.wsh', '.wsc', '.wsf', '.wst',
|
|
'.ps1', '.ps2', '.ps1xml', '.ps2xml', '.psc1', '.psc2',
|
|
'.php', '.phtml', '.phar',
|
|
'.asp', '.aspx', '.cer', '.cfm', '.cgi', '.pl', '.py', '.rb',
|
|
'.jar', '.war', '.ear',
|
|
'.sh', '.bash', '.zsh', '.fish',
|
|
'.msi', '.appx', '.appxbundle',
|
|
'.dll', '.sys', '.drv', '.ocx',
|
|
'.html', '.htm', '.xhtml', '.shtml',
|
|
'.svg', '.xml', '.xsl', '.xslt'
|
|
];
|
|
|
|
const MAGIC_BYTES = {
|
|
'image/jpeg': ['FF D8 FF'],
|
|
'image/png': ['89 50 4E 47 0D 0A 1A 0A'],
|
|
'image/gif': ['47 49 46 38'],
|
|
'image/webp': ['52 49 46 46'],
|
|
'application/pdf': ['25 50 44 46'],
|
|
'application/zip': ['50 4B 03 04', '50 4B 05 06', '50 4B 07 08']
|
|
};
|
|
|
|
class SecureFileUploader {
|
|
constructor() {
|
|
this.ensureUploadDir();
|
|
}
|
|
|
|
async ensureUploadDir() {
|
|
try {
|
|
await fs.mkdir(UPLOAD_DIR, { recursive: true, mode: 0o750 });
|
|
} catch (err) {
|
|
if (err.code !== 'EEXIST') throw err;
|
|
}
|
|
}
|
|
|
|
async uploadFile(file, options = {}) {
|
|
const {
|
|
allowedTypes = Object.keys(ALLOWED_MIME_TYPES),
|
|
maxSize = MAX_FILE_SIZE,
|
|
generateFilename = true,
|
|
userId = 'anonymous'
|
|
} = options;
|
|
|
|
// Validate file presence
|
|
if (!file || !file.buffer) {
|
|
throw new Error('No file provided');
|
|
}
|
|
|
|
// Validate file size
|
|
if (file.buffer.length > maxSize) {
|
|
throw new Error(`File size exceeds maximum of ${maxSize / 1024 / 1024}MB`);
|
|
}
|
|
|
|
// Get file extension
|
|
const originalName = file.originalname || 'unknown';
|
|
const ext = path.extname(originalName).toLowerCase();
|
|
|
|
// Check dangerous extensions
|
|
if (DANGEROUS_EXTENSIONS.includes(ext)) {
|
|
throw new Error(`File type ${ext} is not allowed`);
|
|
}
|
|
|
|
// Detect MIME type from magic bytes
|
|
const detectedMimeType = await this.detectMimeType(file.buffer);
|
|
|
|
if (!allowedTypes.includes(detectedMimeType)) {
|
|
throw new Error(`File type ${detectedMimeType} is not allowed`);
|
|
}
|
|
|
|
// Validate extension matches MIME type
|
|
if (!this.validateExtensionMimeMatch(detectedMimeType, ext)) {
|
|
throw new Error('File extension does not match detected file type');
|
|
}
|
|
|
|
// Generate secure filename
|
|
let filename;
|
|
if (generateFilename) {
|
|
const timestamp = Date.now();
|
|
const randomSuffix = crypto.randomBytes(8).toString('hex');
|
|
const safeOriginalName = originalName.replace(/[^a-zA-Z0-9.-]/g, '_').substring(0, 100);
|
|
filename = `${timestamp}-${randomSuffix}-${safeOriginalName}`;
|
|
} else {
|
|
filename = originalName;
|
|
}
|
|
|
|
// Sanitize filename
|
|
filename = this.sanitizeFilename(filename);
|
|
|
|
// Get user directory
|
|
const userDir = path.join(UPLOAD_DIR, userId);
|
|
await fs.mkdir(userDir, { recursive: true, mode: 0o750 });
|
|
|
|
// Final path
|
|
const filePath = path.join(userDir, filename);
|
|
|
|
// Write file
|
|
await fs.writeFile(filePath, file.buffer, { mode: 0o640 });
|
|
|
|
// Return file info
|
|
return {
|
|
filename,
|
|
originalName,
|
|
path: filePath,
|
|
size: file.buffer.length,
|
|
mimeType: detectedMimeType,
|
|
extension: ext,
|
|
checksum: crypto.createHash('sha256').update(file.buffer).digest('hex')
|
|
};
|
|
}
|
|
|
|
async detectMimeType(buffer) {
|
|
const header = buffer.slice(0, 16).toString('hex').toUpperCase();
|
|
|
|
for (const [mimeType, signatures] of Object.entries(MAGIC_BYTES)) {
|
|
for (const sig of signatures) {
|
|
const cleanSig = sig.replace(/\s/g, '');
|
|
if (header.startsWith(cleanSig)) {
|
|
return mimeType;
|
|
}
|
|
}
|
|
}
|
|
|
|
return 'application/octet-stream';
|
|
}
|
|
|
|
validateExtensionMimeMatch(mimeType, extension) {
|
|
const allowedExtensions = ALLOWED_MIME_TYPES[mimeType] || [];
|
|
return allowedExtensions.includes(extension);
|
|
}
|
|
|
|
sanitizeFilename(filename) {
|
|
return filename
|
|
.replace(/[^a-zA-Z0-9._-]/g, '_')
|
|
.replace(/\.{2,}/g, '.')
|
|
.replace(/^_+/g, '')
|
|
.replace(/_+$/g, '')
|
|
.substring(0, 200);
|
|
}
|
|
|
|
async validateUploadedFile(filePath) {
|
|
const stats = await fs.stat(filePath);
|
|
|
|
// Check file size
|
|
if (stats.size > MAX_FILE_SIZE) {
|
|
throw new Error('File size exceeds limit');
|
|
}
|
|
|
|
// Check if file exists
|
|
if (!await this.fileExists(filePath)) {
|
|
throw new Error('File not found');
|
|
}
|
|
|
|
// Read and validate content
|
|
const buffer = await fs.readFile(filePath);
|
|
const mimeType = await this.detectMimeType(buffer);
|
|
|
|
// Check for malicious content
|
|
if (await this.containsMaliciousContent(buffer)) {
|
|
await fs.unlink(filePath);
|
|
throw new Error('File contains malicious content');
|
|
}
|
|
|
|
return {
|
|
valid: true,
|
|
mimeType,
|
|
size: stats.size
|
|
};
|
|
}
|
|
|
|
async containsMaliciousContent(buffer) {
|
|
// Check for PHP tags
|
|
if (buffer.includes('<?php') || buffer.includes('<?=') || buffer.includes('<%')) {
|
|
return true;
|
|
}
|
|
|
|
// Check for shell scripts
|
|
if (buffer.includes('#!/bin/bash') || buffer.includes('#!/bin/sh')) {
|
|
return true;
|
|
}
|
|
|
|
// Check for null bytes
|
|
if (buffer.includes('\0')) {
|
|
return true;
|
|
}
|
|
|
|
// Check for executable headers (MZ for Windows executables)
|
|
if (buffer.length > 2 && buffer[0] === 0x4D && buffer[1] === 0x5A) {
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
async fileExists(filePath) {
|
|
try {
|
|
await fs.access(filePath, fs.constants.F_OK);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = new SecureFileUploader();
|
|
```
|
|
|
|
##### Step 4.1.2: File Upload Route with Protection
|
|
```javascript
|
|
// src/routes/upload.js
|
|
const express = require('express');
|
|
const multer = require('multer');
|
|
const { requireUserAuth } = require('../middleware/auth');
|
|
const fileUploader = require('../utils/fileUploader');
|
|
const { auditLog } = require('../middleware/auditLogger');
|
|
const crypto = require('crypto');
|
|
|
|
const router = express.Router();
|
|
|
|
// Configure multer for memory storage
|
|
const upload = multer({
|
|
storage: multer.memoryStorage(),
|
|
limits: {
|
|
fileSize: 50 * 1024 * 1024, // 50MB
|
|
files: 5
|
|
},
|
|
fileFilter: (req, file, cb) => {
|
|
const allowedMimeTypes = [
|
|
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
|
|
'application/pdf',
|
|
'text/plain', 'text/markdown',
|
|
'application/zip'
|
|
];
|
|
|
|
if (!allowedMimeTypes.includes(file.mimetype)) {
|
|
return cb(new Error(`File type ${file.mimetype} is not allowed`));
|
|
}
|
|
|
|
cb(null, true);
|
|
}
|
|
});
|
|
|
|
// Upload single file
|
|
router.post('/single', requireUserAuth, upload.single('file'), async (req, res) => {
|
|
try {
|
|
if (!req.file) {
|
|
return res.status(400).json({ error: 'No file uploaded' });
|
|
}
|
|
|
|
const fileInfo = await fileUploader.uploadFile(req.file, {
|
|
userId: req.user.id
|
|
});
|
|
|
|
await auditLog(req, 'FILE_UPLOAD', 'file', fileInfo.checksum, null, {
|
|
filename: fileInfo.originalName,
|
|
size: fileInfo.size,
|
|
mimeType: fileInfo.mimeType
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
file: fileInfo
|
|
});
|
|
} catch (error) {
|
|
res.status(400).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Upload multiple files
|
|
router.post('/multiple', requireUserAuth, upload.array('files', 5), async (req, res) => {
|
|
try {
|
|
if (!req.files || req.files.length === 0) {
|
|
return res.status(400).json({ error: 'No files uploaded' });
|
|
}
|
|
|
|
const uploadedFiles = [];
|
|
|
|
for (const file of req.files) {
|
|
const fileInfo = await fileUploader.uploadFile(file, {
|
|
userId: req.user.id
|
|
});
|
|
uploadedFiles.push(fileInfo);
|
|
}
|
|
|
|
await auditLog(req, 'MULTIPLE_FILES_UPLOAD', 'files', req.user.id, null, {
|
|
count: uploadedFiles.length,
|
|
files: uploadedFiles.map(f => f.filename)
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
files: uploadedFiles
|
|
});
|
|
} catch (error) {
|
|
res.status(400).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Download file (with access control)
|
|
router.get('/:filename', requireUserAuth, async (req, res) => {
|
|
try {
|
|
const { filename } = req.params;
|
|
|
|
// Validate filename
|
|
if (!filename || filename.includes('..') || filename.includes('/')) {
|
|
return res.status(400).json({ error: 'Invalid filename' });
|
|
}
|
|
|
|
const filePath = path.join(UPLOAD_DIR, req.user.id, filename);
|
|
|
|
if (!await fileUploader.fileExists(filePath)) {
|
|
return res.status(404).json({ error: 'File not found' });
|
|
}
|
|
|
|
// Validate file
|
|
await fileUploader.validateUploadedFile(filePath);
|
|
|
|
// Send file
|
|
res.download(filePath, filename);
|
|
} catch (error) {
|
|
res.status(400).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
// Delete file
|
|
router.delete('/:filename', requireUserAuth, async (req, res) => {
|
|
try {
|
|
const { filename } = req.params;
|
|
|
|
// Validate filename
|
|
if (!filename || filename.includes('..') || filename.includes('/')) {
|
|
return res.status(400).json({ error: 'Invalid filename' });
|
|
}
|
|
|
|
const filePath = path.join(UPLOAD_DIR, req.user.id, filename);
|
|
|
|
if (!await fileUploader.fileExists(filePath)) {
|
|
return res.status(404).json({ error: 'File not found' });
|
|
}
|
|
|
|
// Delete file
|
|
await fs.unlink(filePath);
|
|
|
|
await auditLog(req, 'FILE_DELETE', 'file', filename);
|
|
|
|
res.json({ success: true, message: 'File deleted' });
|
|
} catch (error) {
|
|
res.status(400).json({ error: error.message });
|
|
}
|
|
});
|
|
|
|
module.exports = router;
|
|
```
|
|
|
|
#### Verification Steps
|
|
1. Test file type validation
|
|
2. Test magic byte detection
|
|
3. Test dangerous extension blocking
|
|
4. Verify file content scanning
|
|
5. Test upload limits
|
|
|
|
---
|
|
|
|
## Phase 5: Security Headers & Infrastructure
|
|
|
|
### 5.1 Comprehensive Security Headers Implementation
|
|
|
|
**Priority:** High
|
|
**Estimated Effort:** 8 hours
|
|
**Risk Level:** Low
|
|
|
|
#### Remediation Steps
|
|
|
|
##### Step 5.1.1: Enhanced Security Headers Configuration
|
|
```javascript
|
|
// src/config/securityHeaders.js
|
|
const helmet = require('helmet');
|
|
|
|
function createSecurityHeadersConfig() {
|
|
return {
|
|
// Content Security Policy
|
|
contentSecurityPolicy: {
|
|
directives: {
|
|
defaultSrc: ["'self'"],
|
|
scriptSrc: [
|
|
"'self'",
|
|
"'unsafe-inline'", // Required for inline scripts - consider refactoring
|
|
"https://cdn.jsdelivr.net",
|
|
"https://apis.google.com"
|
|
],
|
|
styleSrc: [
|
|
"'self'",
|
|
"'unsafe-inline'",
|
|
"https://fonts.googleapis.com",
|
|
"https://cdn.jsdelivr.net"
|
|
],
|
|
fontSrc: [
|
|
"'self'",
|
|
"https://fonts.gstatic.com",
|
|
"https://cdn.jsdelivr.net"
|
|
],
|
|
imgSrc: [
|
|
"'self'",
|
|
"data:",
|
|
"https:",
|
|
"blob:"
|
|
],
|
|
connectSrc: [
|
|
"'self'",
|
|
"https://api.openrouter.ai",
|
|
"https://api.mistral.ai",
|
|
"https://api.groq.com"
|
|
],
|
|
frameSrc: [
|
|
"'self'",
|
|
"https://www.youtube.com",
|
|
"https://player.vimeo.com"
|
|
],
|
|
objectSrc: ["'none'"],
|
|
mediaSrc: ["'self'", "blob:"],
|
|
workerSrc: ["'self'", "blob:"],
|
|
manifestSrc: ["'self'"],
|
|
prefetchSrc: ["'self'"],
|
|
baseUri: ["'self'"],
|
|
formAction: ["'self'"],
|
|
upgradeInsecureRequests: []
|
|
}
|
|
},
|
|
|
|
// Prevent clickjacking
|
|
crossOriginEmbedderPolicy: false,
|
|
|
|
// Referrer policy
|
|
referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
|
|
|
|
// DNS prefetch control
|
|
dnsPrefetchControl: { allow: false },
|
|
|
|
// IE compatibility
|
|
xContentTypeOptions: true,
|
|
|
|
// Frame blocking
|
|
frameguard: { action: 'deny' },
|
|
|
|
// Power status
|
|
powersaveBlock: false,
|
|
|
|
// Cross-origin policies
|
|
crossOriginResourcePolicy: { policy: 'cross-origin' },
|
|
|
|
// Cross-origin opener policy
|
|
crossOriginOpenerPolicy: { policy: 'same-origin' },
|
|
|
|
// Permissions policy
|
|
permissionsPolicy: {
|
|
features: {
|
|
accelerometer: [],
|
|
camera: [],
|
|
geolocation: [],
|
|
gyroscope: [],
|
|
magnetometer: [],
|
|
microphone: [],
|
|
payment: [],
|
|
usb: []
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
module.exports = { createSecurityHeadersConfig };
|
|
```
|
|
|
|
##### Step 5.1.2: Security Headers Middleware
|
|
```javascript
|
|
// src/middleware/securityHeaders.js
|
|
const { createSecurityHeadersConfig } = require('../config/securityHeaders');
|
|
|
|
function securityHeaders(req, res, next) {
|
|
// Prevent information disclosure
|
|
res.removeHeader('X-Powered-By');
|
|
|
|
// Basic security headers
|
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
|
res.setHeader('X-Frame-Options', 'DENY');
|
|
res.setHeader('X-XSS-Protection', '1; mode=block');
|
|
res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
|
|
res.setHeader('Permissions-Policy', 'geolocation=(), microphone=(), camera=()');
|
|
|
|
// Cache control for sensitive pages
|
|
if (req.path.includes('/account') || req.path.includes('/admin')) {
|
|
res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate');
|
|
res.setHeader('Pragma', 'no-cache');
|
|
res.setHeader('Expires', '0');
|
|
}
|
|
|
|
next();
|
|
}
|
|
|
|
module.exports = { securityHeaders };
|
|
```
|
|
|
|
#### Verification Steps
|
|
1. Test all security headers with curl
|
|
2. Verify CSP blocks XSS attempts
|
|
3. Test clickjacking protection
|
|
4. Verify headers don't break functionality
|
|
|
|
---
|
|
|
|
### 5.2 Docker Security Hardening
|
|
|
|
**Priority:** High
|
|
**Estimated Effort:** 12 hours
|
|
**Risk Level:** Medium
|
|
|
|
#### Remediation Steps
|
|
|
|
##### Step 5.2.1: Enhanced Dockerfile
|
|
```dockerfile
|
|
# Enhanced Dockerfile with security hardening
|
|
FROM ubuntu:24.04 AS base
|
|
|
|
# Security: Set environment variables
|
|
ENV DEBIAN_FRONTEND=noninteractive
|
|
ENV NODE_ENV=production
|
|
ENV NPM_CONFIG_LOGLEVEL=warn
|
|
|
|
# Security: Create non-root user
|
|
RUN groupadd --gid 1000 appgroup && \
|
|
useradd --uid 1000 --gid appgroup --shell /bin/bash --create-home appuser
|
|
|
|
# Security: Install only necessary packages
|
|
RUN apt-get update && \
|
|
apt-get install -y --no-install-recommends \
|
|
ca-certificates \
|
|
curl \
|
|
gnupg \
|
|
logrotate \
|
|
net-tools \
|
|
openssl \
|
|
powershell-7.4 \
|
|
ttdl \
|
|
unzip \
|
|
wget \
|
|
zip \
|
|
&& rm -rf /var/lib/apt/lists/* \
|
|
&& apt-get clean
|
|
|
|
# Security: Install Node.js from official package
|
|
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
|
|
&& apt-get install -y nodejs \
|
|
&& npm install -g npm@latest \
|
|
&& npm config set audit false \
|
|
&& npm config set fund false
|
|
|
|
# Security: Set working directory
|
|
WORKDIR /home/web
|
|
|
|
# Security: Copy application with correct permissions
|
|
COPY --chown=appuser:appgroup package*.json ./
|
|
COPY --chown=appuser:appgroup chat ./chat/
|
|
COPY --chown=appuser:appgroup scripts ./scripts/
|
|
COPY --chown=appuser:appgroup opencode ./opencode/
|
|
|
|
# Security: Install dependencies as non-root user
|
|
USER appuser
|
|
|
|
WORKDIR /home/web/chat
|
|
|
|
# Install Node.js dependencies
|
|
RUN npm ci --only=production && \
|
|
npm cache clean --force
|
|
|
|
# Security: Set environment variables
|
|
ENV PORT=4500
|
|
ENV HOST=0.0.0.0
|
|
ENV NODE_ENV=production
|
|
|
|
# Expose application port
|
|
EXPOSE 4500
|
|
|
|
# Health check
|
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
|
CMD curl -f http://localhost:4500/health || exit 1
|
|
|
|
# Security: Use non-root user to run application
|
|
USER appuser
|
|
|
|
# Run application
|
|
CMD ["node", "server.js"]
|
|
```
|
|
|
|
##### Step 5.2.2: Enhanced Docker Compose
|
|
```yaml
|
|
# docker-compose.yml - Security enhanced version
|
|
version: '3.8'
|
|
|
|
services:
|
|
shopify-ai:
|
|
build: .
|
|
container_name: shopify-ai-secure
|
|
restart: unless-stopped
|
|
ports:
|
|
- "4500:4500"
|
|
environment:
|
|
- NODE_ENV=production
|
|
- PORT=4500
|
|
- HOST=0.0.0.0
|
|
- SESSION_SECRET=${SESSION_SECRET:?err}
|
|
- JWT_SECRET=${JWT_SECRET:?err}
|
|
- DATABASE_ENCRYPTION_KEY=${DATABASE_ENCRYPTION_KEY:?err}
|
|
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY:?err}
|
|
- MISTRAL_API_KEY=${MISTRAL_API_KEY:?err}
|
|
- GROQ_API_KEY=${GROQ_API_KEY:?err}
|
|
- DODO_PAYMENTS_API_KEY=${DODO_PAYMENTS_API_KEY}
|
|
- DODO_PAYMENTS_WEBHOOK_KEY=${DODO_PAYMENTS_WEBHOOK_KEY}
|
|
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
|
|
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET}
|
|
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID}
|
|
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET}
|
|
- SMTP_HOST=${SMTP_HOST}
|
|
- SMTP_PORT=${SMTP_PORT}
|
|
- SMTP_USER=${SMTP_USER}
|
|
- SMTP_PASS=${SMTP_PASS}
|
|
- ALLOWED_ORIGINS=${ALLOWED_ORIGINS:-http://localhost:4500}
|
|
volumes:
|
|
- app-data:/home/web/data
|
|
- app-logs:/home/web/logs
|
|
- app-uploads:/home/web/data/uploads
|
|
deploy:
|
|
resources:
|
|
limits:
|
|
memory: 1.5G
|
|
cpus: '1.0'
|
|
reservations:
|
|
memory: 512M
|
|
cpus: '0.25'
|
|
security_opt:
|
|
- no-new-privileges:true
|
|
cap_drop:
|
|
- ALL
|
|
read_only: true
|
|
tmpfs:
|
|
- /tmp:size=10M,mode=1777
|
|
healthcheck:
|
|
test: ["CMD", "curl", "-f", "http://localhost:4500/health"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 10s
|
|
|
|
volumes:
|
|
app-data:
|
|
driver: local
|
|
driver_opts:
|
|
type: none
|
|
o: bind
|
|
device: ${DATA_PATH:-./data}
|
|
app-logs:
|
|
driver: local
|
|
app-uploads:
|
|
driver: local
|
|
driver_opts:
|
|
type: none
|
|
o: bind
|
|
device: ${UPLOADS_PATH:-./uploads}
|
|
|
|
secrets:
|
|
session_secret:
|
|
file: ./secrets/session_secret.txt
|
|
jwt_secret:
|
|
file: ./secrets/jwt_secret.txt
|
|
```
|
|
|
|
#### Verification Steps
|
|
1. Build and test Docker image
|
|
2. Verify non-root user
|
|
3. Test security constraints
|
|
4. Verify resource limits
|
|
|
|
---
|
|
|
|
## Phase 6: Logging & Monitoring Enhancement
|
|
|
|
### 6.1 Comprehensive Security Logging
|
|
|
|
**Priority:** Medium
|
|
**Estimated Effort:** 12 hours
|
|
**Risk Level:** Low
|
|
|
|
#### Remediation Steps
|
|
|
|
##### Step 6.1.1: Security Event Logger
|
|
```javascript
|
|
// src/utils/securityLogger.js
|
|
const winston = require('winston');
|
|
const path = require('path');
|
|
|
|
const LOG_DIR = process.env.LOG_DIR || '/home/web/logs';
|
|
|
|
// Custom format for security events
|
|
const securityFormat = winston.format.combine(
|
|
winston.format.timestamp({ format: 'YYYY-MM-DD HH:mm:ss.SSS' }),
|
|
winston.format.errors({ stack: true }),
|
|
winston.format.json()
|
|
);
|
|
|
|
// Security event logger
|
|
const securityLogger = winston.createLogger({
|
|
level: 'info',
|
|
format: securityFormat,
|
|
defaultMeta: { service: 'shopify-ai-security' },
|
|
transports: [
|
|
// Error logs
|
|
new winston.transports.File({
|
|
filename: path.join(LOG_DIR, 'security-errors.log'),
|
|
level: 'error',
|
|
maxsize: 10485760, // 10MB
|
|
maxFiles: 10,
|
|
tailable: true
|
|
}),
|
|
// All security events
|
|
new winston.transports.File({
|
|
filename: path.join(LOG_DIR, 'security-events.log'),
|
|
maxsize: 10485760, // 10MB
|
|
maxFiles: 30,
|
|
tailable: true
|
|
}),
|
|
// Failed authentication attempts
|
|
new winston.transports.File({
|
|
filename: path.join(LOG_DIR, 'auth-failures.log'),
|
|
level: 'warn',
|
|
maxsize: 10485760,
|
|
maxFiles: 100,
|
|
tailable: true
|
|
}),
|
|
// Audit log for compliance
|
|
new winston.transports.File({
|
|
filename: path.join(LOG_DIR, 'audit.log'),
|
|
level: 'info',
|
|
maxsize: 10485760,
|
|
maxFiles: 365, // Keep 1 year
|
|
tailable: true
|
|
})
|
|
]
|
|
});
|
|
|
|
// Add console transport in development
|
|
if (process.env.NODE_ENV !== 'production') {
|
|
securityLogger.add(new winston.transports.Console({
|
|
format: winston.format.combine(
|
|
winston.format.colorize(),
|
|
winston.format.simple()
|
|
)
|
|
}));
|
|
}
|
|
|
|
// Security event types
|
|
const SecurityEvents = {
|
|
// Authentication events
|
|
LOGIN_SUCCESS: 'AUTH_LOGIN_SUCCESS',
|
|
LOGIN_FAILURE: 'AUTH_LOGIN_FAILURE',
|
|
LOGOUT: 'AUTH_LOGOUT',
|
|
PASSWORD_CHANGE: 'AUTH_PASSWORD_CHANGE',
|
|
PASSWORD_RESET_REQUEST: 'AUTH_PASSWORD_RESET_REQUEST',
|
|
PASSWORD_RESET_COMPLETE: 'AUTH_PASSWORD_RESET_COMPLETE',
|
|
ACCOUNT_LOCKED: 'AUTH_ACCOUNT_LOCKED',
|
|
ACCOUNT_UNLOCKED: 'AUTH_ACCOUNT_UNLOCKED',
|
|
TWO_FACTOR_ENABLED: 'AUTH_2FA_ENABLED',
|
|
TWO_FACTOR_DISABLED: 'AUTH_2FA_DISABLED',
|
|
SESSION_CREATED: 'AUTH_SESSION_CREATED',
|
|
SESSION_REVOKED: 'AUTH_SESSION_REVOKED',
|
|
|
|
// Authorization events
|
|
UNAUTHORIZED_ACCESS: 'AUTH_UNAUTHORIZED_ACCESS',
|
|
FORBIDDEN_ACCESS: 'AUTH_FORBIDDEN_ACCESS',
|
|
ADMIN_ACTION: 'AUTH_ADMIN_ACTION',
|
|
|
|
// Data events
|
|
DATA_EXPORT: 'DATA_EXPORT',
|
|
DATA_DELETE: 'DATA_DELETE',
|
|
DATA_UPDATE: 'DATA_UPDATE',
|
|
FILE_UPLOAD: 'FILE_UPLOAD',
|
|
FILE_DOWNLOAD: 'FILE_DOWNLOAD',
|
|
FILE_DELETE: 'FILE_DELETE',
|
|
|
|
// Security events
|
|
RATE_LIMIT_EXCEEDED: 'SEC_RATE_LIMIT_EXCEEDED',
|
|
SUSPICIOUS_ACTIVITY: 'SEC_SUSPICIOUS_ACTIVITY',
|
|
PROMPT_INJECTION: 'SEC_PROMPT_INJECTION',
|
|
XSS_ATTEMPT: 'SEC_XSS_ATTEMPT',
|
|
SQL_INJECTION_ATTEMPT: 'SEC_SQL_INJECTION_ATTEMPT',
|
|
FILE_UPLOAD_BLOCKED: 'SEC_FILE_UPLOAD_BLOCKED',
|
|
|
|
// System events
|
|
CONFIG_CHANGE: 'SYS_CONFIG_CHANGE',
|
|
API_KEY_ACCESS: 'SYS_API_KEY_ACCESS'
|
|
};
|
|
|
|
// Log security event
|
|
function logSecurityEvent(eventType, metadata = {}) {
|
|
const event = {
|
|
eventType,
|
|
timestamp: new Date().toISOString(),
|
|
...metadata
|
|
};
|
|
|
|
// Determine log level based on event type
|
|
let level = 'info';
|
|
if (eventType.startsWith('AUTH_FAILURE') || eventType.startsWith('SEC_')) {
|
|
level = 'warn';
|
|
}
|
|
if (eventType.includes('ERROR')) {
|
|
level = 'error';
|
|
}
|
|
|
|
securityLogger.log(level, event);
|
|
|
|
return event;
|
|
}
|
|
|
|
// Log authentication event
|
|
function logAuthEvent(eventType, userId, req, additionalData = {}) {
|
|
logSecurityEvent(eventType, {
|
|
userId,
|
|
ip: req.ip || req.headers['x-forwarded-for'],
|
|
userAgent: req.headers['user-agent'],
|
|
path: req.path,
|
|
method: req.method,
|
|
...additionalData
|
|
});
|
|
}
|
|
|
|
module.exports = {
|
|
securityLogger,
|
|
SecurityEvents,
|
|
logSecurityEvent,
|
|
logAuthEvent
|
|
};
|
|
```
|
|
|
|
##### Step 6.1.2: Security Monitoring Middleware
|
|
```javascript
|
|
// src/middleware/securityMonitoring.js
|
|
const { logSecurityEvent, SecurityEvents } = require('../utils/securityLogger');
|
|
|
|
function securityMonitoring(req, res, next) {
|
|
const startTime = Date.now();
|
|
|
|
// Capture original end function
|
|
const originalEnd = res.end;
|
|
let responseBody;
|
|
|
|
res.end = function(chunk, encoding) {
|
|
responseBody = chunk;
|
|
return originalEnd.apply(this, arguments);
|
|
};
|
|
|
|
res.on('finish', () => {
|
|
const duration = Date.now() - startTime;
|
|
|
|
// Log suspicious response patterns
|
|
if (res.statusCode === 404 && req.path.startsWith('/api/')) {
|
|
logSecurityEvent(SecurityEvents.SUSPICIOUS_ACTIVITY, {
|
|
userId: req.user?.id || 'anonymous',
|
|
ip: req.ip,
|
|
path: req.path,
|
|
method: req.method,
|
|
reason: '404 on API endpoint'
|
|
});
|
|
}
|
|
|
|
// Log slow requests (potential DoS)
|
|
if (duration > 10000 && req.path.startsWith('/api/')) {
|
|
logSecurityEvent(SecurityEvents.SUSPICIOUS_ACTIVITY, {
|
|
userId: req.user?.id || 'anonymous',
|
|
ip: req.ip,
|
|
path: req.path,
|
|
method: req.method,
|
|
duration,
|
|
reason: 'Slow request detected'
|
|
});
|
|
}
|
|
});
|
|
|
|
next();
|
|
}
|
|
|
|
module.exports = { securityMonitoring };
|
|
```
|
|
|
|
#### Verification Steps
|
|
1. Verify all security events are logged
|
|
2. Test log rotation
|
|
3. Verify log integrity
|
|
4. Test alert generation
|
|
|
|
---
|
|
|
|
## Phase 7: API Security Enhancement
|
|
|
|
### 7.1 Enhanced Rate Limiting with Fingerprinting
|
|
|
|
**Priority:** Medium
|
|
**Estimated Effort:** 8 hours
|
|
**Risk Level:** Low
|
|
|
|
#### Remediation Steps
|
|
|
|
##### Step 7.1.1: Advanced Rate Limiter
|
|
```javascript
|
|
// src/middleware/advancedRateLimiter.js
|
|
const rateLimit = require('express-rate-limit');
|
|
const crypto = require('crypto');
|
|
|
|
function createAdvancedRateLimiter(options = {}) {
|
|
const {
|
|
windowMs = 60 * 1000,
|
|
max = 100,
|
|
message = 'Too many requests',
|
|
keyGenerator = null
|
|
} = options;
|
|
|
|
// Store for rate limiting (use Redis in production)
|
|
const rateLimitStore = new Map();
|
|
|
|
const limiter = rateLimit({
|
|
windowMs,
|
|
max,
|
|
message: {
|
|
error: message,
|
|
retryAfter: Math.ceil(windowMs / 1000)
|
|
},
|
|
standardHeaders: true,
|
|
legacyHeaders: false,
|
|
|
|
keyGenerator: (req) => {
|
|
// Create fingerprint combining IP and User-Agent
|
|
if (keyGenerator) {
|
|
return keyGenerator(req);
|
|
}
|
|
|
|
const userAgent = req.headers['user-agent'] || 'unknown';
|
|
const ip = req.ip || req.headers['x-forwarded-for'] || 'unknown';
|
|
|
|
const fingerprint = crypto
|
|
.createHash('sha256')
|
|
.update(`${ip}:${userAgent}`)
|
|
.digest('hex')
|
|
.substring(0, 32);
|
|
|
|
return fingerprint;
|
|
},
|
|
|
|
skip: (req) => {
|
|
// Skip for health checks
|
|
return req.path === '/health' || req.path === '/api/health';
|
|
},
|
|
|
|
handler: (req, res, options) => {
|
|
// Log rate limit violations
|
|
logSecurityEvent(SecurityEvents.RATE_LIMIT_EXCEEDED, {
|
|
userId: req.user?.id || 'anonymous',
|
|
ip: req.ip,
|
|
userAgent: req.headers['user-agent'],
|
|
path: req.path,
|
|
method: req.method
|
|
});
|
|
|
|
res.status(429).json({
|
|
error: options.message,
|
|
retryAfter: options.message.retryAfter
|
|
});
|
|
}
|
|
});
|
|
|
|
return limiter;
|
|
}
|
|
|
|
// Specialized rate limiters
|
|
const generalRateLimiter = createAdvancedRateLimiter({
|
|
windowMs: 60 * 1000,
|
|
max: 100
|
|
});
|
|
|
|
const authRateLimiter = createAdvancedRateLimiter({
|
|
windowMs: 15 * 60 * 1000,
|
|
max: 5,
|
|
message: 'Too many authentication attempts'
|
|
});
|
|
|
|
const apiRateLimiter = createAdvancedRateLimiter({
|
|
windowMs: 60 * 1000,
|
|
max: 60
|
|
});
|
|
|
|
const uploadRateLimiter = createAdvancedRateLimiter({
|
|
windowMs: 60 * 60 * 1000,
|
|
max: 20,
|
|
message: 'Too many file uploads'
|
|
});
|
|
|
|
module.exports = {
|
|
createAdvancedRateLimiter,
|
|
generalRateLimiter,
|
|
authRateLimiter,
|
|
apiRateLimiter,
|
|
uploadRateLimiter
|
|
};
|
|
```
|
|
|
|
#### Verification Steps
|
|
1. Test rate limiting thresholds
|
|
2. Verify fingerprint-based limiting
|
|
3. Test bypass attempts
|
|
4. Check 429 response format
|
|
|
|
---
|
|
|
|
## Phase 8: Compliance & Documentation
|
|
|
|
### 8.1 Security Documentation Update
|
|
|
|
**Priority:** Medium
|
|
**Estimated Effort:** 8 hours
|
|
**Risk Level:** Low
|
|
|
|
#### Remediation Steps
|
|
|
|
##### Step 8.1.1: Update Security Documentation
|
|
Create comprehensive security documentation including:
|
|
1. Incident Response Plan
|
|
2. Security Architecture Document
|
|
3. Encryption Key Management Policy
|
|
4. Access Control Matrix
|
|
5. Security Audit Procedures
|
|
6. Vulnerability Disclosure Policy
|
|
|
|
##### Step 8.1.2: Security Runbook
|
|
Document procedures for:
|
|
1. Responding to security incidents
|
|
2. Revoking compromised credentials
|
|
3. Handling data breaches
|
|
4. Password reset procedures
|
|
5. Account recovery processes
|
|
|
|
---
|
|
|
|
## Implementation Timeline
|
|
|
|
### Phase 1: Critical Security (Weeks 1-2)
|
|
| Task | Effort | Priority | Dependencies |
|
|
|------|--------|----------|--------------|
|
|
| Express.js Framework Migration | 40h | Critical | None |
|
|
| Database with Encryption | 60h | Critical | None |
|
|
| Session Revocation | 20h | Critical | Database |
|
|
|
|
### Phase 2: Authentication (Weeks 2-3)
|
|
| Task | Effort | Priority | Dependencies |
|
|
|------|--------|----------|--------------|
|
|
| Password Enhancement | 16h | High | None |
|
|
| Two-Factor Authentication | 24h | High | Database |
|
|
|
|
### Phase 3: Input Validation (Weeks 3-4)
|
|
| Task | Effort | Priority | Dependencies |
|
|
|------|--------|----------|--------------|
|
|
| Sanitization Framework | 20h | High | None |
|
|
| AI Prompt Protection | 16h | High | None |
|
|
|
|
### Phase 4: File Security (Week 4)
|
|
| Task | Effort | Priority | Dependencies |
|
|
|------|--------|----------|--------------|
|
|
| Secure File Upload | 16h | High | None |
|
|
|
|
### Phase 5: Infrastructure (Weeks 5-6)
|
|
| Task | Effort | Priority | Dependencies |
|
|
|------|--------|----------|--------------|
|
|
| Security Headers | 8h | High | None |
|
|
| Docker Hardening | 12h | High | None |
|
|
|
|
### Phase 6-8: Logging & Compliance (Weeks 6-8)
|
|
| Task | Effort | Priority | Dependencies |
|
|
|------|--------|----------|--------------|
|
|
| Security Logging | 12h | Medium | None |
|
|
| Rate Limiting | 8h | Medium | None |
|
|
| Documentation | 8h | Medium | All phases |
|
|
|
|
---
|
|
|
|
## Total Effort Estimate
|
|
|
|
| Phase | Hours |
|
|
|-------|-------|
|
|
| Phase 1: Critical Infrastructure | 120h |
|
|
| Phase 2: Authentication Security | 40h |
|
|
| Phase 3: Input Validation | 36h |
|
|
| Phase 4: File Upload Security | 16h |
|
|
| Phase 5: Infrastructure Security | 20h |
|
|
| Phase 6: Logging & Monitoring | 12h |
|
|
| Phase 7: API Security | 8h |
|
|
| Phase 8: Documentation | 8h |
|
|
| **Total** | **260 hours** |
|
|
|
|
---
|
|
|
|
## Testing Requirements
|
|
|
|
### Security Testing Checklist
|
|
1. [ ] Penetration testing on all endpoints
|
|
2. [ ] Fuzz testing for input validation
|
|
3. [ ] Load testing with rate limits
|
|
4. [ ] Authentication flow testing
|
|
5. [ ] File upload vulnerability testing
|
|
6. [ ] Session management testing
|
|
7. [ ] API security testing
|
|
8. [ ] Compliance verification
|
|
|
|
### Automated Security Testing
|
|
1. [ ] Integrate npm audit into CI/CD
|
|
2. [ ] Implement SAST scanning
|
|
3. [ ] Configure dependency vulnerability scanning
|
|
4. [ ] Add security headers validation to tests
|
|
5. [ ] Implement security regression testing
|
|
|
|
---
|
|
|
|
## Rollback Procedures
|
|
|
|
Each phase should include:
|
|
1. Database backups before changes
|
|
2. Configuration snapshots
|
|
3. Feature flags for gradual rollout
|
|
4. Immediate rollback procedures
|
|
5. Communication plan for users
|
|
|
|
---
|
|
|
|
## Approval Required
|
|
|
|
This security remediation plan requires:
|
|
- [ ] Security team review
|
|
- [ ] Development team approval
|
|
- [ ] Operations team sign-off
|
|
- [ ] Management budget approval (~$260,000 based on 260 hours)
|
|
- [ ] Compliance team acknowledgment
|
|
|
|
---
|
|
|
|
## Document Control
|
|
|
|
| Version | Date | Author | Changes |
|
|
|---------|------|--------|---------|
|
|
| 1.0 | 2026-02-08 | Security Team | Initial plan |
|
|
|
|
---
|
|
|
|
**End of Document**
|