mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-18 06:23:18 +00:00
fix(security): harden encryption key permissions, increase password minimum, remove sensitive logs (#323)
Self-heal encryption key file permissions to 0600 on startup. Increase minimum password length from 6 to 8 characters per NIST SP 800-63B. Remove console.log statements that exposed file paths, .env locations, stack names, and admin usernames to stdout.
This commit is contained in:
+9
-10
@@ -47,6 +47,7 @@ const _origEmitWarning = process.emitWarning.bind(process);
|
||||
_origEmitWarning(warning, ...args);
|
||||
};
|
||||
|
||||
const MIN_PASSWORD_LENGTH = 8;
|
||||
const app = express();
|
||||
const PORT = 3000;
|
||||
|
||||
@@ -353,8 +354,8 @@ app.post('/api/auth/setup', authRateLimiter, async (req: Request, res: Response)
|
||||
return;
|
||||
}
|
||||
|
||||
if (password.length < 6) {
|
||||
res.status(400).json({ error: 'Password must be at least 6 characters' });
|
||||
if (password.length < MIN_PASSWORD_LENGTH) {
|
||||
res.status(400).json({ error: `Password must be at least ${MIN_PASSWORD_LENGTH} characters` });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -432,8 +433,8 @@ app.put('/api/auth/password', authMiddleware, async (req: Request, res: Response
|
||||
res.status(400).json({ error: 'Old password and new password are required' });
|
||||
return;
|
||||
}
|
||||
if (newPassword.length < 6) {
|
||||
res.status(400).json({ error: 'New password must be at least 6 characters' });
|
||||
if (newPassword.length < MIN_PASSWORD_LENGTH) {
|
||||
res.status(400).json({ error: `New password must be at least ${MIN_PASSWORD_LENGTH} characters` });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1797,8 +1798,8 @@ app.post('/api/users', authMiddleware, async (req: Request, res: Response): Prom
|
||||
res.status(400).json({ error: 'Username must be at least 3 characters (letters, numbers, underscore, hyphen)' });
|
||||
return;
|
||||
}
|
||||
if (typeof password !== 'string' || password.length < 6) {
|
||||
res.status(400).json({ error: 'Password must be at least 6 characters' });
|
||||
if (typeof password !== 'string' || password.length < MIN_PASSWORD_LENGTH) {
|
||||
res.status(400).json({ error: `Password must be at least ${MIN_PASSWORD_LENGTH} characters` });
|
||||
return;
|
||||
}
|
||||
const validRoles: UserRole[] = ['admin', 'viewer', 'deployer', 'node-admin', 'auditor'];
|
||||
@@ -1888,8 +1889,8 @@ app.put('/api/users/:id', authMiddleware, async (req: Request, res: Response): P
|
||||
}
|
||||
|
||||
if (password !== undefined) {
|
||||
if (typeof password !== 'string' || password.length < 6) {
|
||||
res.status(400).json({ error: 'Password must be at least 6 characters' });
|
||||
if (typeof password !== 'string' || password.length < MIN_PASSWORD_LENGTH) {
|
||||
res.status(400).json({ error: `Password must be at least ${MIN_PASSWORD_LENGTH} characters` });
|
||||
return;
|
||||
}
|
||||
updates.password_hash = await bcrypt.hash(password, 10);
|
||||
@@ -2491,13 +2492,11 @@ app.put('/api/stacks/:stackName', async (req: Request, res: Response) => {
|
||||
}
|
||||
try {
|
||||
const { content } = req.body;
|
||||
console.log('PUT /api/stacks/:stackName', { stackName, contentType: typeof content, contentLength: content?.length });
|
||||
if (typeof content !== 'string') {
|
||||
console.error('Content is not a string:', content);
|
||||
return res.status(400).json({ error: 'Content must be a string' });
|
||||
}
|
||||
await FileSystemService.getInstance(req.nodeId).saveStackContent(stackName, content);
|
||||
console.log('Stack saved successfully:', stackName);
|
||||
res.json({ message: 'Stack saved successfully' });
|
||||
} catch (error) {
|
||||
console.error('Failed to save stack:', error);
|
||||
|
||||
@@ -17,6 +17,16 @@ export class CryptoService {
|
||||
|
||||
if (fs.existsSync(keyPath)) {
|
||||
this.key = Buffer.from(fs.readFileSync(keyPath, 'utf-8').trim(), 'hex');
|
||||
// Self-heal permissive file permissions (no-op on Windows)
|
||||
try {
|
||||
const mode = fs.statSync(keyPath).mode & 0o777;
|
||||
if (mode !== 0o600) {
|
||||
console.warn(`[CryptoService] Fixing permissive key file permissions (was 0o${mode.toString(8)}, set to 0o600)`);
|
||||
fs.chmodSync(keyPath, 0o600);
|
||||
}
|
||||
} catch {
|
||||
// chmod not supported on this platform (e.g. Windows) — skip
|
||||
}
|
||||
} else {
|
||||
this.key = crypto.randomBytes(KEY_LENGTH);
|
||||
if (!fs.existsSync(dataDir)) {
|
||||
|
||||
@@ -473,7 +473,7 @@ export class DatabaseService {
|
||||
this.db.prepare(
|
||||
'INSERT INTO users (username, password_hash, role, created_at, updated_at) VALUES (?, ?, ?, ?, ?)'
|
||||
).run(username, passwordHash, 'admin', now, now);
|
||||
console.log(`Migrated admin user "${username}" to users table.`);
|
||||
console.log('Migrated legacy admin user to users table.');
|
||||
}
|
||||
|
||||
private migrateJsonConfig(dataDir: string) {
|
||||
|
||||
@@ -85,10 +85,8 @@ export class FileSystemService {
|
||||
|
||||
async saveStackContent(stackName: string, content: string): Promise<void> {
|
||||
const filePath = path.join(this.baseDir, stackName, 'compose.yaml');
|
||||
console.log('Saving to path:', filePath);
|
||||
try {
|
||||
await fsPromises.writeFile(filePath, content, 'utf-8');
|
||||
console.log('File written successfully');
|
||||
} catch (error) {
|
||||
console.error('Error writing file:', error);
|
||||
throw new Error(`Failed to save stack: ${stackName}`);
|
||||
@@ -128,10 +126,8 @@ export class FileSystemService {
|
||||
|
||||
async saveEnvContent(stackName: string, content: string): Promise<void> {
|
||||
const envPath = path.join(this.baseDir, stackName, '.env');
|
||||
console.log('Saving env to path:', envPath);
|
||||
try {
|
||||
await fsPromises.writeFile(envPath, content, 'utf-8');
|
||||
console.log('Env file written successfully');
|
||||
} catch (error) {
|
||||
console.error('Error writing env file:', error);
|
||||
throw new Error(`Failed to save env file for stack: ${stackName}`);
|
||||
@@ -163,7 +159,6 @@ export class FileSystemService {
|
||||
`;
|
||||
try {
|
||||
await fsPromises.writeFile(path.join(stackDir, 'compose.yaml'), boilerplate, 'utf-8');
|
||||
console.log('Stack created successfully:', stackName);
|
||||
} catch (error) {
|
||||
console.error('Error creating stack:', error);
|
||||
throw new Error(`Failed to create stack: ${stackName}`);
|
||||
@@ -174,7 +169,6 @@ export class FileSystemService {
|
||||
const stackDir = path.join(this.baseDir, stackName);
|
||||
try {
|
||||
await fsPromises.rm(stackDir, { recursive: true, force: true });
|
||||
console.log('Stack deleted successfully:', stackName);
|
||||
} catch (error: unknown) {
|
||||
const fsError = error as NodeJS.ErrnoException;
|
||||
if (fsError.code === 'ENOENT') return;
|
||||
@@ -188,9 +182,8 @@ export class FileSystemService {
|
||||
try {
|
||||
await fsPromises.rmdir(stackDir);
|
||||
} catch {
|
||||
console.warn(`[FileSystemService] Could not remove empty directory ${stackDir} — may need manual cleanup`);
|
||||
console.warn('[FileSystemService] Could not remove empty directory after Docker fallback — may need manual cleanup');
|
||||
}
|
||||
console.log('Stack deleted successfully (via Docker fallback):', stackName);
|
||||
} else {
|
||||
console.error('Error deleting stack directory:', fsError.message);
|
||||
throw new Error(`Failed to delete stack directory: ${fsError.message}`);
|
||||
@@ -251,7 +244,6 @@ export class FileSystemService {
|
||||
try {
|
||||
await fsPromises.access(this.baseDir);
|
||||
} catch {
|
||||
console.log('Creating compose directory:', this.baseDir);
|
||||
await fsPromises.mkdir(this.baseDir, { recursive: true });
|
||||
return;
|
||||
}
|
||||
@@ -267,13 +259,11 @@ export class FileSystemService {
|
||||
|
||||
try {
|
||||
await fsPromises.access(stackDir);
|
||||
console.log(`Skipping migration for "${stackName}": directory already exists`);
|
||||
continue;
|
||||
} catch {
|
||||
// Directory doesn't exist, proceed
|
||||
}
|
||||
|
||||
console.log(`Migrating stack: ${stackName}`);
|
||||
await fsPromises.mkdir(stackDir, { recursive: true });
|
||||
|
||||
const oldComposePath = path.join(this.baseDir, item.name);
|
||||
@@ -285,12 +275,10 @@ export class FileSystemService {
|
||||
try {
|
||||
await fsPromises.access(oldEnvPath);
|
||||
await fsPromises.rename(oldEnvPath, newEnvPath);
|
||||
console.log(`Migrated env file for: ${stackName}`);
|
||||
} catch {
|
||||
// No env file to migrate
|
||||
}
|
||||
|
||||
console.log(`Successfully migrated stack: ${stackName}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Migration error:', error);
|
||||
|
||||
Reference in New Issue
Block a user