fix(security): eliminate default admin password vulnerability
BREAKING CHANGE: Admin password is now auto-generated on first setup Security improvements: - Remove hardcoded 'admin123' password completely - Generate secure random password on first installation - Save credentials to ADMIN_CREDENTIALS.txt (git-ignored) - Force password change on first login - Implement strong password requirements (12+ chars, mixed case, numbers, special) - Add password strength validation - Increase bcrypt rounds from 10 to 12 New features: - Password generator utility with secure random generation - Human-readable password format (e.g., SwiftEagle3847\!) - Password reset script for existing installations - Comprehensive admin setup documentation - Must-change-password flag in database Migration guide: - New installations: Check ADMIN_CREDENTIALS.txt for generated password - Existing installations: Run scripts/reset-admin-password.js - All users must change password on first login after update This fixes a critical vulnerability where all installations used the same default admin password, allowing unauthorized access. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,25 @@
|
||||
const { db } = require('../src/database/db');
|
||||
|
||||
async function addMustChangePasswordColumn() {
|
||||
try {
|
||||
// Check if the column already exists
|
||||
const hasMustChangePassword = await db.schema.hasColumn('admin_users', 'must_change_password');
|
||||
|
||||
if (!hasMustChangePassword) {
|
||||
await db.schema.table('admin_users', (table) => {
|
||||
table.boolean('must_change_password').defaultTo(false);
|
||||
});
|
||||
|
||||
console.log('✅ Added must_change_password column to admin_users table');
|
||||
} else {
|
||||
console.log('ℹ️ must_change_password column already exists');
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('❌ Migration failed:', error);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
addMustChangePasswordColumn();
|
||||
@@ -1,5 +1,8 @@
|
||||
const bcrypt = require('bcrypt');
|
||||
const { db, initializeDatabase } = require('../src/database/db');
|
||||
const { generateReadablePassword } = require('../src/utils/passwordGenerator');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
|
||||
async function runMigrations() {
|
||||
console.log('Running database migrations...');
|
||||
@@ -11,19 +14,54 @@ async function runMigrations() {
|
||||
// Create default admin user if none exists
|
||||
const adminExists = await db('admin_users').first();
|
||||
if (!adminExists) {
|
||||
const defaultPassword = 'admin123'; // Change this!
|
||||
const passwordHash = await bcrypt.hash(defaultPassword, 10);
|
||||
// Generate a secure random password
|
||||
const generatedPassword = generateReadablePassword();
|
||||
const passwordHash = await bcrypt.hash(generatedPassword, 12); // Increased rounds for better security
|
||||
|
||||
await db('admin_users').insert({
|
||||
username: 'admin',
|
||||
email: '[email protected]',
|
||||
password_hash: passwordHash
|
||||
password_hash: passwordHash,
|
||||
must_change_password: true, // Flag for forcing password change
|
||||
created_at: new Date()
|
||||
});
|
||||
|
||||
console.log('Default admin user created:');
|
||||
// Save the generated password to a file for the user to retrieve
|
||||
const setupInfoPath = path.join(__dirname, '..', '..', 'ADMIN_CREDENTIALS.txt');
|
||||
const setupInfo = `
|
||||
========================================
|
||||
PicPeak Admin Credentials
|
||||
========================================
|
||||
|
||||
Your admin account has been created with these credentials:
|
||||
|
||||
Username: admin
|
||||
Password: ${generatedPassword}
|
||||
|
||||
IMPORTANT SECURITY NOTES:
|
||||
1. You MUST change this password on first login
|
||||
2. This file will be created only once
|
||||
3. Store these credentials securely
|
||||
4. Delete this file after noting the password
|
||||
|
||||
Login URL: ${process.env.ADMIN_URL || 'http://localhost:3001'}/admin
|
||||
|
||||
Generated on: ${new Date().toISOString()}
|
||||
========================================
|
||||
`;
|
||||
|
||||
await fs.writeFile(setupInfoPath, setupInfo, 'utf8');
|
||||
|
||||
console.log('\n========================================');
|
||||
console.log('✅ Admin user created successfully!');
|
||||
console.log('========================================');
|
||||
console.log('Username: admin');
|
||||
console.log('Password: admin123');
|
||||
console.log('⚠️ Please change this password immediately!');
|
||||
console.log(`Password: ${generatedPassword}`);
|
||||
console.log('\n⚠️ IMPORTANT:');
|
||||
console.log('1. Save these credentials securely');
|
||||
console.log('2. You will be required to change the password on first login');
|
||||
console.log('3. Credentials are also saved in: ADMIN_CREDENTIALS.txt');
|
||||
console.log('========================================\n');
|
||||
}
|
||||
|
||||
// Create default email templates if none exist
|
||||
|
||||
Executable
+109
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const bcrypt = require('bcrypt');
|
||||
const { db } = require('../src/database/db');
|
||||
const { generateReadablePassword } = require('../src/utils/passwordGenerator');
|
||||
const fs = require('fs').promises;
|
||||
const path = require('path');
|
||||
const readline = require('readline');
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout
|
||||
});
|
||||
|
||||
async function question(prompt) {
|
||||
return new Promise((resolve) => {
|
||||
rl.question(prompt, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
async function resetAdminPassword() {
|
||||
console.log('\n========================================');
|
||||
console.log('PicPeak Admin Password Reset Tool');
|
||||
console.log('========================================\n');
|
||||
|
||||
try {
|
||||
// Check if admin user exists
|
||||
const admin = await db('admin_users')
|
||||
.where({ username: 'admin' })
|
||||
.first();
|
||||
|
||||
if (!admin) {
|
||||
console.error('❌ No admin user found in the database.');
|
||||
console.log('Run migrations first: npm run migrate');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('Found admin user:', admin.username);
|
||||
console.log('Email:', admin.email);
|
||||
console.log('\nThis will reset the password for this admin account.');
|
||||
|
||||
const confirm = await question('\nDo you want to continue? (yes/no): ');
|
||||
|
||||
if (confirm.toLowerCase() !== 'yes' && confirm.toLowerCase() !== 'y') {
|
||||
console.log('\n❌ Password reset cancelled.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Generate new password
|
||||
const newPassword = generateReadablePassword();
|
||||
const passwordHash = await bcrypt.hash(newPassword, 12);
|
||||
|
||||
// Update the admin user
|
||||
await db('admin_users')
|
||||
.where({ username: 'admin' })
|
||||
.update({
|
||||
password_hash: passwordHash,
|
||||
must_change_password: true,
|
||||
updated_at: new Date()
|
||||
});
|
||||
|
||||
// Save to file
|
||||
const resetInfoPath = path.join(__dirname, '..', '..', 'ADMIN_PASSWORD_RESET.txt');
|
||||
const resetInfo = `
|
||||
========================================
|
||||
PicPeak Admin Password Reset
|
||||
========================================
|
||||
|
||||
Password has been reset for admin account:
|
||||
|
||||
Username: admin
|
||||
New Password: ${newPassword}
|
||||
|
||||
IMPORTANT:
|
||||
1. You MUST change this password on next login
|
||||
2. This file contains sensitive information
|
||||
3. Delete this file after noting the password
|
||||
|
||||
Login URL: ${process.env.ADMIN_URL || 'http://localhost:3001'}/admin
|
||||
|
||||
Reset performed on: ${new Date().toISOString()}
|
||||
========================================
|
||||
`;
|
||||
|
||||
await fs.writeFile(resetInfoPath, resetInfo, 'utf8');
|
||||
|
||||
console.log('\n✅ Password reset successful!\n');
|
||||
console.log('========================================');
|
||||
console.log('New Credentials:');
|
||||
console.log('========================================');
|
||||
console.log('Username: admin');
|
||||
console.log(`Password: ${newPassword}`);
|
||||
console.log('\n⚠️ IMPORTANT:');
|
||||
console.log('1. You will be required to change this password on next login');
|
||||
console.log('2. Credentials are also saved in: ADMIN_PASSWORD_RESET.txt');
|
||||
console.log('3. Delete the file after noting the password');
|
||||
console.log('========================================\n');
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Error resetting password:', error.message);
|
||||
process.exit(1);
|
||||
} finally {
|
||||
rl.close();
|
||||
process.exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
// Run the reset
|
||||
resetAdminPassword();
|
||||
@@ -4,13 +4,14 @@ const { body, validationResult } = require('express-validator');
|
||||
const { db, logActivity } = require('../database/db');
|
||||
const { adminAuth } = require('../middleware/auth');
|
||||
const { endSession } = require('../middleware/sessionTimeout');
|
||||
const { validatePasswordStrength } = require('../utils/passwordGenerator');
|
||||
const router = express.Router();
|
||||
|
||||
// Change password
|
||||
router.post('/change-password', [
|
||||
adminAuth,
|
||||
body('currentPassword').notEmpty().withMessage('Current password is required'),
|
||||
body('newPassword').isLength({ min: 6 }).withMessage('New password must be at least 6 characters')
|
||||
body('newPassword').isLength({ min: 12 }).withMessage('New password must be at least 12 characters')
|
||||
], async (req, res) => {
|
||||
try {
|
||||
const errors = validationResult(req);
|
||||
@@ -21,6 +22,15 @@ router.post('/change-password', [
|
||||
const { currentPassword, newPassword } = req.body;
|
||||
const userId = req.admin.id; // Changed from req.user.id to req.admin.id
|
||||
|
||||
// Validate new password strength
|
||||
const passwordValidation = validatePasswordStrength(newPassword);
|
||||
if (!passwordValidation.isValid) {
|
||||
return res.status(400).json({
|
||||
error: 'Password does not meet security requirements',
|
||||
details: passwordValidation.messages
|
||||
});
|
||||
}
|
||||
|
||||
// Get user from database
|
||||
const user = await db('admin_users')
|
||||
.where('id', userId)
|
||||
@@ -36,14 +46,15 @@ router.post('/change-password', [
|
||||
return res.status(400).json({ error: 'Current password is incorrect' });
|
||||
}
|
||||
|
||||
// Hash new password
|
||||
const newPasswordHash = await bcrypt.hash(newPassword, 10);
|
||||
// Hash new password with more rounds
|
||||
const newPasswordHash = await bcrypt.hash(newPassword, 12);
|
||||
|
||||
// Update password
|
||||
// Update password and clear must_change_password flag
|
||||
await db('admin_users')
|
||||
.where('id', userId)
|
||||
.update({
|
||||
password_hash: newPasswordHash,
|
||||
must_change_password: false,
|
||||
updated_at: new Date()
|
||||
});
|
||||
|
||||
|
||||
@@ -41,14 +41,15 @@ router.post('/admin/login', [
|
||||
// Update last login
|
||||
await db('admin_users').where('id', admin.id).update({ last_login: new Date() });
|
||||
|
||||
const token = jwt.sign({ id: admin.id }, process.env.JWT_SECRET, { expiresIn: '24h' });
|
||||
const token = jwt.sign({ id: admin.id, type: 'admin' }, process.env.JWT_SECRET, { expiresIn: '24h' });
|
||||
|
||||
res.json({
|
||||
token,
|
||||
user: {
|
||||
id: admin.id,
|
||||
username: admin.username,
|
||||
email: admin.email
|
||||
email: admin.email,
|
||||
mustChangePassword: admin.must_change_password || false
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,39 +1,122 @@
|
||||
const crypto = require('crypto');
|
||||
|
||||
/**
|
||||
* Generate a secure random password
|
||||
* @param {number} length - Password length (default 12)
|
||||
* @param {number} length - Password length (default: 16)
|
||||
* @returns {string} Generated password
|
||||
*/
|
||||
function generatePassword(length = 12) {
|
||||
function generateSecurePassword(length = 16) {
|
||||
const charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()_+-=[]{}|;:,.<>?';
|
||||
let password = '';
|
||||
|
||||
// Ensure at least one of each required character type
|
||||
const lowercase = 'abcdefghijklmnopqrstuvwxyz';
|
||||
const uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
const numbers = '0123456789';
|
||||
const symbols = '!@#$%&*';
|
||||
const special = '!@#$%^&*()_+-=[]{}|;:,.<>?';
|
||||
|
||||
// Ensure at least one character from each set
|
||||
const requiredChars = [
|
||||
lowercase[Math.floor(Math.random() * lowercase.length)],
|
||||
uppercase[Math.floor(Math.random() * uppercase.length)],
|
||||
numbers[Math.floor(Math.random() * numbers.length)],
|
||||
symbols[Math.floor(Math.random() * symbols.length)]
|
||||
];
|
||||
// Add one of each required type
|
||||
password += lowercase[crypto.randomInt(lowercase.length)];
|
||||
password += uppercase[crypto.randomInt(uppercase.length)];
|
||||
password += numbers[crypto.randomInt(numbers.length)];
|
||||
password += special[crypto.randomInt(special.length)];
|
||||
|
||||
// Fill the rest with random characters from all sets
|
||||
const allChars = lowercase + uppercase + numbers + symbols;
|
||||
const remainingLength = length - requiredChars.length;
|
||||
|
||||
let password = '';
|
||||
for (let i = 0; i < remainingLength; i++) {
|
||||
password += allChars[Math.floor(Math.random() * allChars.length)];
|
||||
// Fill the rest randomly
|
||||
for (let i = password.length; i < length; i++) {
|
||||
password += charset[crypto.randomInt(charset.length)];
|
||||
}
|
||||
|
||||
// Combine and shuffle
|
||||
const passwordArray = [...requiredChars, ...password];
|
||||
for (let i = passwordArray.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[passwordArray[i], passwordArray[j]] = [passwordArray[j], passwordArray[i]];
|
||||
}
|
||||
|
||||
return passwordArray.join('');
|
||||
// Shuffle the password
|
||||
return password.split('').sort(() => crypto.randomInt(3) - 1).join('');
|
||||
}
|
||||
|
||||
module.exports = { generatePassword };
|
||||
/**
|
||||
* Generate a human-readable password using words and numbers
|
||||
* @returns {string} Generated password
|
||||
*/
|
||||
function generateReadablePassword() {
|
||||
const adjectives = [
|
||||
'Swift', 'Bright', 'Strong', 'Happy', 'Clever',
|
||||
'Brave', 'Noble', 'Quick', 'Sharp', 'Bold'
|
||||
];
|
||||
|
||||
const nouns = [
|
||||
'Eagle', 'Mountain', 'River', 'Thunder', 'Forest',
|
||||
'Ocean', 'Falcon', 'Dragon', 'Phoenix', 'Tiger'
|
||||
];
|
||||
|
||||
const adjective = adjectives[crypto.randomInt(adjectives.length)];
|
||||
const noun = nouns[crypto.randomInt(nouns.length)];
|
||||
const number = crypto.randomInt(1000, 9999);
|
||||
const special = '!@#$%'[crypto.randomInt(5)];
|
||||
|
||||
return `${adjective}${noun}${number}${special}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate password strength
|
||||
* @param {string} password - Password to validate
|
||||
* @returns {object} Validation result with score and messages
|
||||
*/
|
||||
function validatePasswordStrength(password) {
|
||||
const result = {
|
||||
score: 0,
|
||||
messages: [],
|
||||
isValid: false
|
||||
};
|
||||
|
||||
// Length check
|
||||
if (password.length < 8) {
|
||||
result.messages.push('Password must be at least 8 characters long');
|
||||
} else if (password.length < 12) {
|
||||
result.score += 1;
|
||||
} else {
|
||||
result.score += 2;
|
||||
}
|
||||
|
||||
// Character type checks
|
||||
if (!/[a-z]/.test(password)) {
|
||||
result.messages.push('Password must contain lowercase letters');
|
||||
} else {
|
||||
result.score += 1;
|
||||
}
|
||||
|
||||
if (!/[A-Z]/.test(password)) {
|
||||
result.messages.push('Password must contain uppercase letters');
|
||||
} else {
|
||||
result.score += 1;
|
||||
}
|
||||
|
||||
if (!/[0-9]/.test(password)) {
|
||||
result.messages.push('Password must contain numbers');
|
||||
} else {
|
||||
result.score += 1;
|
||||
}
|
||||
|
||||
if (!/[!@#$%^&*()_+\-=\[\]{}|;:,.<>?]/.test(password)) {
|
||||
result.messages.push('Password must contain special characters');
|
||||
} else {
|
||||
result.score += 1;
|
||||
}
|
||||
|
||||
// Common password check
|
||||
const commonPasswords = [
|
||||
'password', 'admin123', '12345678', 'qwerty', 'abc123',
|
||||
'password123', 'admin', 'letmein', 'welcome', 'monkey'
|
||||
];
|
||||
|
||||
if (commonPasswords.includes(password.toLowerCase())) {
|
||||
result.score = 0;
|
||||
result.messages.push('Password is too common');
|
||||
}
|
||||
|
||||
result.isValid = result.score >= 4 && result.messages.length === 0;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
generateSecurePassword,
|
||||
generateReadablePassword,
|
||||
validatePasswordStrength
|
||||
};
|
||||
Reference in New Issue
Block a user