Files
picpeak/backend/scripts/safe-auth-deployment.js
T
paul e35ac6a41c
Test Gitea Actions / test (push) Successful in 20s
continuous-integration/drone/push Build is passing
feat: implement critical security fixes for SQL injection and authentication vulnerabilities
Security Enhancements:
- Fix SQL injection vulnerabilities by replacing whereRaw queries with parameterized queries
- Add LIKE pattern escaping to prevent SQL injection in search functionality
- Implement account lockout protection (5 failed attempts = 30 min lockout)
- Add comprehensive login attempt tracking and audit trail
- Enhance JWT tokens with issuer validation, IP tracking, and password change detection
- Add logout endpoint and session management
- Prevent user enumeration with generic error messages

Database Changes:
- Add login_attempts table for authentication tracking
- Add security columns to admin_users (password_changed_at, last_login_ip, two_factor_enabled)

New Security Features:
- Brute force protection with configurable lockout duration
- Automatic cleanup of old login attempts
- Enhanced authentication middleware with stricter validation
- Monitoring scripts for security health checks

All fixes are backward compatible and production-ready with rollback plans included.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-13 00:40:05 +02:00

269 lines
8.3 KiB
JavaScript
Executable File

#!/usr/bin/env node
/**
* Safe Authentication Deployment Script
* Carefully activates auth security with multiple safety checks
*/
const { db } = require('../src/database/db');
const logger = require('../src/utils/logger');
console.log('=== Safe Authentication Security Deployment ===\n');
// Color codes
const GREEN = '\x1b[32m';
const RED = '\x1b[31m';
const YELLOW = '\x1b[33m';
const BLUE = '\x1b[34m';
const RESET = '\x1b[0m';
async function runSafetyChecks() {
console.log(`${BLUE}Running pre-deployment safety checks...${RESET}\n`);
const checks = {
databaseConnected: false,
adminUsersExist: false,
activeSessionsExist: false,
migrationsReady: true,
diskSpace: true
};
try {
// Check 1: Database connection
await db.raw('SELECT 1');
checks.databaseConnected = true;
console.log(`${GREEN}${RESET} Database connection healthy`);
} catch (e) {
console.log(`${RED}${RESET} Database connection failed`);
return checks;
}
try {
// Check 2: Admin users exist
const adminCount = await db('admin_users').count('id as count').first();
checks.adminUsersExist = adminCount.count > 0;
console.log(`${GREEN}${RESET} Found ${adminCount.count} admin users`);
} catch (e) {
console.log(`${RED}${RESET} Could not check admin users`);
}
try {
// Check 3: Check for active sessions (optional warning)
const recentLogins = await db('access_logs')
.where('action', 'login_success')
.where('timestamp', '>', new Date(Date.now() - 60 * 60 * 1000).toISOString())
.count('id as count')
.first();
if (recentLogins.count > 0) {
checks.activeSessionsExist = true;
console.log(`${YELLOW}!${RESET} Warning: ${recentLogins.count} active sessions in last hour`);
} else {
console.log(`${GREEN}${RESET} No recent active sessions`);
}
} catch (e) {
// Table might not exist yet, that's ok
console.log(`${GREEN}${RESET} No access logs table yet (expected)`);
}
try {
// Check 4: Check if migrations would conflict
const tables = await db.raw(`
SELECT name FROM sqlite_master
WHERE type='table' AND name IN ('login_attempts')
`);
if (tables.length > 0) {
console.log(`${YELLOW}!${RESET} login_attempts table already exists`);
checks.migrationsReady = false;
} else {
console.log(`${GREEN}${RESET} Ready to create login_attempts table`);
}
} catch (e) {
console.log(`${RED}${RESET} Could not check existing tables`);
checks.migrationsReady = false;
}
return checks;
}
async function backupDatabase() {
console.log(`\n${BLUE}Creating database backup...${RESET}`);
try {
const fs = require('fs').promises;
const path = require('path');
const dbPath = process.env.DB_PATH || './data/database.db';
const backupPath = `${dbPath}.backup.${Date.now()}`;
await fs.copyFile(dbPath, backupPath);
console.log(`${GREEN}${RESET} Database backed up to: ${path.basename(backupPath)}`);
return backupPath;
} catch (e) {
console.log(`${YELLOW}!${RESET} Could not create backup: ${e.message}`);
return null;
}
}
async function runMigrations() {
console.log(`\n${BLUE}Running database migrations...${RESET}`);
try {
// Run migrations
const knex = db;
await knex.migrate.latest();
console.log(`${GREEN}${RESET} Migrations completed successfully`);
// Verify tables exist
const loginAttempts = await db('login_attempts').count().first();
console.log(`${GREEN}${RESET} login_attempts table created`);
const adminColumns = await db('admin_users').columnInfo();
if (adminColumns.password_changed_at) {
console.log(`${GREEN}${RESET} Security columns added to admin_users`);
}
return true;
} catch (e) {
console.log(`${RED}${RESET} Migration failed: ${e.message}`);
return false;
}
}
async function testEnhancedAuth() {
console.log(`\n${BLUE}Testing enhanced authentication (without activating)...${RESET}`);
try {
// Test that enhanced modules load correctly
const authSecurity = require('../src/utils/authSecurity');
const authEnhanced = require('../src/middleware/auth-enhanced');
const authRoutes = require('../src/routes/auth-enhanced');
console.log(`${GREEN}${RESET} Enhanced auth modules load correctly`);
// Test lockout logic (without real data)
const lockoutStatus = await authSecurity.checkAccountLockout('test-user-that-doesnt-exist');
console.log(`${GREEN}${RESET} Account lockout check works: ${lockoutStatus.isLocked ? 'locked' : 'not locked'}`);
// Test generic error
const error = authSecurity.getGenericAuthError();
console.log(`${GREEN}${RESET} Generic error message: "${error}"`);
return true;
} catch (e) {
console.log(`${RED}${RESET} Enhanced auth test failed: ${e.message}`);
return false;
}
}
async function createDeploymentInstructions() {
console.log(`\n${BLUE}Deployment Instructions:${RESET}\n`);
const instructions = `
${GREEN}Step 1: Update server.js${RESET}
Change:
${YELLOW}const authRoutes = require('./src/routes/auth');${RESET}
To:
${GREEN}const authRoutes = require('./src/routes/auth-enhanced');${RESET}
Add after database initialization:
${GREEN}const { initializeCleanupJob } = require('./src/utils/authSecurity');
initializeCleanupJob();${RESET}
${GREEN}Step 2: Update middleware imports (if needed)${RESET}
In files using adminAuth, change:
${YELLOW}const { adminAuth } = require('../middleware/auth');${RESET}
To:
${GREEN}const { adminAuth } = require('../middleware/auth-enhanced');${RESET}
${GREEN}Step 3: Restart the application${RESET}
${BLUE}docker-compose restart backend${RESET}
or
${BLUE}pm2 restart picpeak-backend${RESET}
${GREEN}Step 4: Monitor logs${RESET}
${BLUE}docker-compose logs -f backend | grep -i auth${RESET}
${YELLOW}Rollback if needed:${RESET}
Revert server.js changes and restart
`;
console.log(instructions);
}
async function main() {
try {
// Step 1: Run safety checks
const checks = await runSafetyChecks();
if (!checks.databaseConnected) {
console.log(`\n${RED}Cannot proceed: Database not connected${RESET}`);
process.exit(1);
}
if (checks.activeSessionsExist) {
console.log(`\n${YELLOW}Warning: There are active user sessions.`);
console.log(`Consider deploying during low-traffic period.${RESET}`);
}
// Step 2: Backup database
const backupPath = await backupDatabase();
// Step 3: Run migrations
console.log(`\n${YELLOW}Ready to run migrations. This will:`);
console.log(`- Create login_attempts table`);
console.log(`- Add security columns to admin_users`);
console.log(`No existing data will be modified.${RESET}\n`);
const readline = require('readline').createInterface({
input: process.stdin,
output: process.stdout
});
readline.question('Continue with migrations? (y/n): ', async (answer) => {
if (answer.toLowerCase() !== 'y') {
console.log(`${YELLOW}Deployment cancelled${RESET}`);
readline.close();
await db.destroy();
return;
}
const migrationSuccess = await runMigrations();
if (!migrationSuccess) {
console.log(`\n${RED}Migrations failed. Database backup available at: ${backupPath}${RESET}`);
readline.close();
await db.destroy();
return;
}
// Step 4: Test enhanced auth
const authTestSuccess = await testEnhancedAuth();
if (!authTestSuccess) {
console.log(`\n${YELLOW}Enhanced auth tests failed, but migrations succeeded.`);
console.log(`Review the errors before activating enhanced auth.${RESET}`);
}
// Step 5: Show deployment instructions
await createDeploymentInstructions();
console.log(`\n${GREEN}✓ Pre-deployment complete!${RESET}`);
console.log(`${YELLOW}Enhanced auth is ready but NOT YET ACTIVE.${RESET}`);
console.log(`Follow the instructions above to activate when ready.\n`);
readline.close();
await db.destroy();
});
} catch (error) {
console.error(`${RED}Deployment script error:${RESET}`, error);
await db.destroy();
process.exit(1);
}
}
// Run the deployment
main();