security: Fix critical vulnerabilities and harden application
## Security Fixes ### CRITICAL: Command Injection (adminBackup.js) - Replaced exec() with spawn() using argument arrays - Added input sanitization for host, user, and ssh_key - Added regex validation for hostname/IP format - Added username format validation - Added SSH key file existence check - Prevents shell metacharacter injection attacks ### HIGH: Hardcoded Password (set-admin-password.js) - Removed hardcoded 'admin123' password - Now requires password as CLI argument or env variable - Added password strength validation (8+ chars, mixed case, numbers, special chars) - Added --help flag with usage instructions - Invalidates existing sessions on password change ### MEDIUM: XSS Vulnerability (WelcomeMessageEditor.tsx) - Added DOMPurify sanitization to getPreviewHtml() - Strips all HTML tags before rendering preview - Prevents script injection in admin preview ### LOW: Sample Password Exposure (EmailConfigPage.tsx) - Replaced plaintext sample password with masked placeholder - Uses '••••••••' instead of realistic password ## Dependency Updates - Fixed npm audit vulnerabilities (jws, qs, express) - Backend: 0 vulnerabilities - Frontend: 0 vulnerabilities
This commit is contained in:
Generated
+24
-48
@@ -4355,21 +4355,6 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser/node_modules/qs": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||
"integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/body-parser/node_modules/type-is": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
|
||||
@@ -5766,39 +5751,39 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.21.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz",
|
||||
"integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==",
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.3",
|
||||
"content-disposition": "0.5.4",
|
||||
"body-parser": "~1.20.3",
|
||||
"content-disposition": "~0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.7.1",
|
||||
"cookie-signature": "1.0.6",
|
||||
"cookie": "~0.7.1",
|
||||
"cookie-signature": "~1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "1.3.1",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"finalhandler": "~1.3.1",
|
||||
"fresh": "~0.5.2",
|
||||
"http-errors": "~2.0.0",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"on-finished": "~2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.12",
|
||||
"path-to-regexp": "~0.1.12",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.13.0",
|
||||
"qs": "~6.14.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "0.19.0",
|
||||
"serve-static": "1.16.2",
|
||||
"send": "~0.19.0",
|
||||
"serve-static": "~1.16.2",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"statuses": "~2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
"utils-merge": "1.0.1",
|
||||
"vary": "~1.1.2"
|
||||
@@ -5836,15 +5821,6 @@
|
||||
"node": ">= 8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/cookie": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/debug": {
|
||||
"version": "2.6.9",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
|
||||
@@ -7837,12 +7813,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jws": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
|
||||
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz",
|
||||
"integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jwa": "^1.4.1",
|
||||
"jwa": "^1.4.2",
|
||||
"safe-buffer": "^5.0.1"
|
||||
}
|
||||
},
|
||||
@@ -9694,12 +9670,12 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"version": "6.14.1",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
|
||||
"integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.6"
|
||||
"side-channel": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Script to set/reset admin password
|
||||
*
|
||||
* Usage:
|
||||
* node set-admin-password.js <new-password>
|
||||
* node set-admin-password.js --env (uses ADMIN_PASSWORD environment variable)
|
||||
*
|
||||
* Security: Password must be at least 8 characters with mixed case, numbers, and special characters
|
||||
*/
|
||||
|
||||
const bcrypt = require('bcrypt');
|
||||
const path = require('path');
|
||||
require('dotenv').config({ path: path.join(__dirname, '../.env') });
|
||||
@@ -16,22 +26,108 @@ const db = knex({
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Validate password strength
|
||||
*/
|
||||
function validatePassword(password) {
|
||||
if (!password || password.length < 8) {
|
||||
return { valid: false, error: 'Password must be at least 8 characters long' };
|
||||
}
|
||||
if (!/[a-z]/.test(password)) {
|
||||
return { valid: false, error: 'Password must contain at least one lowercase letter' };
|
||||
}
|
||||
if (!/[A-Z]/.test(password)) {
|
||||
return { valid: false, error: 'Password must contain at least one uppercase letter' };
|
||||
}
|
||||
if (!/[0-9]/.test(password)) {
|
||||
return { valid: false, error: 'Password must contain at least one number' };
|
||||
}
|
||||
if (!/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(password)) {
|
||||
return { valid: false, error: 'Password must contain at least one special character' };
|
||||
}
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
function printUsage() {
|
||||
console.log(`
|
||||
Usage:
|
||||
node set-admin-password.js <new-password>
|
||||
node set-admin-password.js --env
|
||||
|
||||
Options:
|
||||
<new-password> The new password to set (must meet security requirements)
|
||||
--env Use ADMIN_PASSWORD environment variable
|
||||
|
||||
Security Requirements:
|
||||
- At least 8 characters
|
||||
- At least one lowercase letter
|
||||
- At least one uppercase letter
|
||||
- At least one number
|
||||
- At least one special character (!@#$%^&*()_+-=[]{}|;':\",./<>?)
|
||||
|
||||
Examples:
|
||||
node set-admin-password.js "MySecure@Pass123"
|
||||
ADMIN_PASSWORD="MySecure@Pass123" node set-admin-password.js --env
|
||||
`);
|
||||
}
|
||||
|
||||
async function setAdminPassword() {
|
||||
try {
|
||||
const password = 'admin123';
|
||||
const hashedPassword = await bcrypt.hash(password, 10);
|
||||
|
||||
await db('admin_users')
|
||||
// Get password from argument or environment
|
||||
const args = process.argv.slice(2);
|
||||
let password;
|
||||
|
||||
if (args.length === 0) {
|
||||
console.error('❌ Error: No password provided\n');
|
||||
printUsage();
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (args[0] === '--env') {
|
||||
password = process.env.ADMIN_PASSWORD;
|
||||
if (!password) {
|
||||
console.error('❌ Error: ADMIN_PASSWORD environment variable not set');
|
||||
process.exit(1);
|
||||
}
|
||||
} else if (args[0] === '--help' || args[0] === '-h') {
|
||||
printUsage();
|
||||
process.exit(0);
|
||||
} else {
|
||||
password = args[0];
|
||||
}
|
||||
|
||||
// Validate password strength
|
||||
const validation = validatePassword(password);
|
||||
if (!validation.valid) {
|
||||
console.error(`❌ Error: ${validation.error}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Hash password
|
||||
const hashedPassword = await bcrypt.hash(password, 12);
|
||||
|
||||
// Update database
|
||||
const updated = await db('admin_users')
|
||||
.where('username', 'admin')
|
||||
.update({
|
||||
password_hash: hashedPassword,
|
||||
password_changed_at: new Date(),
|
||||
updated_at: new Date()
|
||||
});
|
||||
|
||||
console.log('✅ Admin password set to: admin123');
|
||||
|
||||
if (updated === 0) {
|
||||
console.error('❌ Error: Admin user not found');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('✅ Admin password updated successfully');
|
||||
console.log(' Note: All existing sessions have been invalidated');
|
||||
|
||||
await db.destroy();
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
console.error('❌ Error setting password:', error);
|
||||
console.error('❌ Error setting password:', error.message);
|
||||
await db.destroy();
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,25 +230,89 @@ router.post('/test-connection', adminAuth, async (req, res) => {
|
||||
break;
|
||||
|
||||
case 'rsync':
|
||||
// Test rsync connection
|
||||
const { exec } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
const sshCommand = config.ssh_key
|
||||
? `ssh -i ${config.ssh_key} -o StrictHostKeyChecking=no -o ConnectTimeout=10`
|
||||
: 'ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10';
|
||||
|
||||
const testCommand = config.user
|
||||
? `${sshCommand} ${config.user}@${config.host} "echo 'Connection successful'"`
|
||||
: `${sshCommand} ${config.host} "echo 'Connection successful'"`;
|
||||
|
||||
// Test rsync connection using spawn with argument arrays to prevent command injection
|
||||
const { spawn } = require('child_process');
|
||||
|
||||
// Validate and sanitize inputs to prevent command injection
|
||||
const sanitizeInput = (input) => {
|
||||
if (!input || typeof input !== 'string') return null;
|
||||
// Remove any shell metacharacters and limit length
|
||||
return input.replace(/[;&|`$(){}[\]<>\\!#*?"'\n\r]/g, '').substring(0, 255);
|
||||
};
|
||||
|
||||
const host = sanitizeInput(config.host);
|
||||
const user = sanitizeInput(config.user);
|
||||
const sshKeyPath = sanitizeInput(config.ssh_key);
|
||||
|
||||
if (!host) {
|
||||
res.json({ success: false, message: 'Invalid host specified' });
|
||||
break;
|
||||
}
|
||||
|
||||
// Validate host format (hostname or IP only)
|
||||
const hostRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?)*$/;
|
||||
const ipRegex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||||
if (!hostRegex.test(host) && !ipRegex.test(host)) {
|
||||
res.json({ success: false, message: 'Invalid host format' });
|
||||
break;
|
||||
}
|
||||
|
||||
// Validate username format if provided
|
||||
if (user && !/^[a-zA-Z_][a-zA-Z0-9_-]*$/.test(user)) {
|
||||
res.json({ success: false, message: 'Invalid username format' });
|
||||
break;
|
||||
}
|
||||
|
||||
// Build SSH arguments as array (safe from injection)
|
||||
const sshArgs = [];
|
||||
if (sshKeyPath) {
|
||||
// Validate SSH key path exists and is a file
|
||||
const fsSync = require('fs');
|
||||
if (!fsSync.existsSync(sshKeyPath) || !fsSync.statSync(sshKeyPath).isFile()) {
|
||||
res.json({ success: false, message: 'SSH key file not found' });
|
||||
break;
|
||||
}
|
||||
sshArgs.push('-i', sshKeyPath);
|
||||
}
|
||||
sshArgs.push('-o', 'StrictHostKeyChecking=no');
|
||||
sshArgs.push('-o', 'ConnectTimeout=10');
|
||||
sshArgs.push('-o', 'BatchMode=yes');
|
||||
|
||||
// Add target (user@host or just host)
|
||||
const target = user ? `${user}@${host}` : host;
|
||||
sshArgs.push(target);
|
||||
sshArgs.push('echo', 'Connection successful');
|
||||
|
||||
try {
|
||||
const { stdout } = await execAsync(testCommand);
|
||||
const result = await new Promise((resolve, reject) => {
|
||||
const sshProcess = spawn('ssh', sshArgs, {
|
||||
timeout: 15000,
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
|
||||
sshProcess.stdout.on('data', (data) => { stdout += data; });
|
||||
sshProcess.stderr.on('data', (data) => { stderr += data; });
|
||||
|
||||
sshProcess.on('close', (code) => {
|
||||
if (code === 0) {
|
||||
resolve({ success: true, stdout });
|
||||
} else {
|
||||
reject(new Error(stderr || `SSH exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
|
||||
sshProcess.on('error', (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
|
||||
res.json({ success: true, message: 'Rsync connection successful' });
|
||||
} catch (error) {
|
||||
logger.warn('Rsync connection test failed', {
|
||||
destination: config.host || config.destination,
|
||||
destination: host,
|
||||
error: error.message
|
||||
});
|
||||
res.json({ success: false, message: 'Rsync connection failed. Check server logs for details.' });
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import { HelpCircle } from 'lucide-react';
|
||||
import DOMPurify from 'dompurify';
|
||||
|
||||
interface WelcomeMessageEditorProps {
|
||||
value: string;
|
||||
@@ -18,9 +19,16 @@ export const WelcomeMessageEditor: React.FC<WelcomeMessageEditorProps> = ({
|
||||
onChange(e.target.value);
|
||||
};
|
||||
|
||||
// Convert newlines to <br> tags for preview
|
||||
// Convert newlines to <br> tags for preview with XSS sanitization
|
||||
const getPreviewHtml = () => {
|
||||
return value
|
||||
// First sanitize the input to remove any malicious content
|
||||
const sanitized = DOMPurify.sanitize(value, {
|
||||
ALLOWED_TAGS: [], // Strip all HTML tags, only allow text
|
||||
ALLOWED_ATTR: [],
|
||||
KEEP_CONTENT: true
|
||||
});
|
||||
// Then convert newlines to <br> tags
|
||||
return sanitized
|
||||
.split('\n')
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0)
|
||||
|
||||
@@ -227,10 +227,11 @@ export const EmailConfigPage: React.FC = () => {
|
||||
if (!selectedTemplateKey || !editedTemplate) return;
|
||||
|
||||
// Generate sample data based on the template
|
||||
// Note: These are clearly marked placeholder values for template preview only
|
||||
const sampleData: Record<string, string> = {
|
||||
event_name: 'John & Jane Wedding',
|
||||
event_date: 'December 25, 2024',
|
||||
password: 'wedding2024',
|
||||
password: '••••••••', // Masked placeholder for preview
|
||||
gallery_link: 'https://photos.example.com/gallery/john-jane-wedding',
|
||||
expiration_date: 'January 25, 2025',
|
||||
welcome_message: 'Thank you for celebrating our special day with us!',
|
||||
|
||||
Reference in New Issue
Block a user