From f317a83814fda3a98eb009c1a05a955bfadd6f0d Mon Sep 17 00:00:00 2001 From: Anso Date: Wed, 1 Apr 2026 21:27:37 -0400 Subject: [PATCH] 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. --- CHANGELOG.md | 3 ++ backend/src/index.ts | 19 +++++----- backend/src/services/CryptoService.ts | 10 ++++++ backend/src/services/DatabaseService.ts | 2 +- backend/src/services/FileSystemService.ts | 14 +------- docker-entrypoint.sh | 2 ++ docs/images/rbac/user-create-form.png | Bin 0 -> 48699 bytes docs/reference/security-advisories.mdx | 34 ++++++++++++++++++ frontend/src/components/SettingsModal.tsx | 4 +-- frontend/src/components/Setup.tsx | 4 +-- .../src/components/settings/UsersSection.tsx | 6 ++-- 11 files changed, 67 insertions(+), 31 deletions(-) create mode 100644 docs/images/rbac/user-create-form.png diff --git a/CHANGELOG.md b/CHANGELOG.md index e8b5933a..a9e92aad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +* **security:** self-heal encryption key file permissions on startup — verifies `0600` and corrects if permissive; Docker entrypoint also enforces `chmod 600` before privilege drop +* **security:** increase minimum password length from 6 to 8 characters (NIST SP 800-63B) — applies to setup, password change, and user management; existing short passwords remain valid until changed +* **security:** remove sensitive data from console output — file paths, `.env` locations, stack names, and admin usernames no longer logged to stdout * **security:** use raw request bytes for webhook HMAC signature verification instead of re-serialized JSON — prevents signature mismatches from serialization differences * **security:** use NIST-recommended 12-byte IV for AES-256-GCM encryption (backward compatible with existing 16-byte IVs) * **security:** add 1-year default expiry to node proxy JWT tokens — previously issued without expiry diff --git a/backend/src/index.ts b/backend/src/index.ts index 73b44580..78de1fdf 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -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); diff --git a/backend/src/services/CryptoService.ts b/backend/src/services/CryptoService.ts index a09db5d5..e794fa2d 100644 --- a/backend/src/services/CryptoService.ts +++ b/backend/src/services/CryptoService.ts @@ -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)) { diff --git a/backend/src/services/DatabaseService.ts b/backend/src/services/DatabaseService.ts index ccbd3c6b..6a8f9544 100644 --- a/backend/src/services/DatabaseService.ts +++ b/backend/src/services/DatabaseService.ts @@ -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) { diff --git a/backend/src/services/FileSystemService.ts b/backend/src/services/FileSystemService.ts index c842b017..f3b3f98b 100644 --- a/backend/src/services/FileSystemService.ts +++ b/backend/src/services/FileSystemService.ts @@ -85,10 +85,8 @@ export class FileSystemService { async saveStackContent(stackName: string, content: string): Promise { 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 { 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); diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 5026a8bd..50d9b745 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -24,6 +24,8 @@ if [ "$(id -u)" = '0' ]; then mkdir -p "$DATA_DIR" find "$DATA_DIR" \( \! -user sencho -o \! -group sencho \) \ -exec chown sencho:sencho '{}' + + # Restrict encryption key to owner-only access (rw-------) + [ -f "$DATA_DIR/encryption.key" ] && chmod 600 "$DATA_DIR/encryption.key" echo "[entrypoint] Data directory ownership ensured: $DATA_DIR" # 2. Fix Docker socket group access. diff --git a/docs/images/rbac/user-create-form.png b/docs/images/rbac/user-create-form.png new file mode 100644 index 0000000000000000000000000000000000000000..6112111a369013179d723612d600b0b31f71c34c GIT binary patch literal 48699 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 + This is a defense-in-depth measure. In Docker deployments, the non-root `sencho` user and volume isolation already limit exposure. The fix primarily benefits bare-metal or non-containerized deployments. + + +### 2. Minimum password length increased to 8 characters + +**Finding:** The minimum password length was 6 characters, which is below the [NIST SP 800-63B](https://pages.nist.gov/800-63-3/sp800-63b.html) recommendation of at least 8 characters for memorized secrets. + +**Remediation:** The minimum password length has been increased from 6 to 8 characters across all validation points — initial setup, password changes, and user creation/updates. + + + Existing users with passwords shorter than 8 characters can still log in. The new minimum only applies when setting or changing a password. Administrators may want to notify users to update short passwords. + + +### 3. Sensitive data removed from console output + +**Finding:** Several `console.log` statements in the backend exposed full filesystem paths (including paths to `.env` files), stack names, and admin usernames in standard output. In containerized deployments, stdout is often collected by logging aggregators, making this an information disclosure risk. + +**Remediation:** All path-exposing and data-leaking log statements have been removed from `FileSystemService`, `DatabaseService`, and the stack management routes. Error-level logging (`console.error`) for failure diagnostics has been retained, but without sensitive path or identity details. + +--- + ## March 2026 Audit The following vulnerabilities were identified against the Sencho Docker image built with Docker CLI v29.3.1 and Docker Compose v2.40.3. diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx index 0db67909..fc7c1ebd 100644 --- a/frontend/src/components/SettingsModal.tsx +++ b/frontend/src/components/SettingsModal.tsx @@ -175,8 +175,8 @@ export function SettingsModal({ isOpen, onClose }: SettingsModalProps) { toast.error('New passwords do not match'); return; } - if (authData.newPassword.length < 6) { - toast.error('New password must be at least 6 characters'); + if (authData.newPassword.length < 8) { + toast.error('New password must be at least 8 characters'); return; } setIsSavingPassword(true); diff --git a/frontend/src/components/Setup.tsx b/frontend/src/components/Setup.tsx index 9f29152d..988b317d 100644 --- a/frontend/src/components/Setup.tsx +++ b/frontend/src/components/Setup.tsx @@ -35,8 +35,8 @@ export function Setup({ return; } - if (password.length < 6) { - setError('Password must be at least 6 characters'); + if (password.length < 8) { + setError('Password must be at least 8 characters'); return; } diff --git a/frontend/src/components/settings/UsersSection.tsx b/frontend/src/components/settings/UsersSection.tsx index 19375e19..36a53dc3 100644 --- a/frontend/src/components/settings/UsersSection.tsx +++ b/frontend/src/components/settings/UsersSection.tsx @@ -78,8 +78,8 @@ export function UsersSection() { toast.error('Password is required for new users.'); return; } - if (formPassword && formPassword.length < 6) { - toast.error('Password must be at least 6 characters.'); + if (formPassword && formPassword.length < 8) { + toast.error('Password must be at least 8 characters.'); return; } if (formPassword && formPassword !== formConfirmPassword) { @@ -283,7 +283,7 @@ export function UsersSection() { type="password" value={formPassword} onChange={(e) => setFormPassword(e.target.value)} - placeholder={editingUser ? 'Leave blank to keep' : 'min. 6 characters'} + placeholder={editingUser ? 'Leave blank to keep' : 'min. 8 characters'} />