Fix 16 HIGH/MEDIUM security bugs across API

HIGH fixes:
- TOTP disable now requires valid code verification
- TOTP secret removed from plaintext file storage
- Container ID validated before update/check-update/logs operations
- DNS server parameter restricted to configured servers (SSRF prevention)
- Backup export no longer includes encryption key
- Backup restore of sensitive files requires TOTP re-authentication

MEDIUM fixes:
- Session cookie Secure flag added
- Caddy reload errors no longer leaked to client
- saveConfig uses atomic locked updates via configStateManager
- Log file path traversal prevented via symlink resolution
- Credential cache entries now expire after 5 minutes
- _httpFetch enforces 10MB response size limit
- External URL path injection into Caddyfile blocked
- Custom volume host paths validated against allowed roots
- Error logs endpoint no longer returns stack traces
- Logo delete path traversal prevented via path.basename()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-07 00:15:28 -08:00
parent 6979302fb7
commit 59b6d7d360
12 changed files with 172 additions and 69 deletions

View File

@@ -15,7 +15,8 @@ const CREDENTIALS_FILE = process.env.CREDENTIALS_FILE || path.join(__dirname, 'c
class CredentialManager {
constructor() {
this.useKeychain = keychainManager.available;
this.cache = new Map(); // In-memory cache for performance
this.cache = new Map(); // In-memory cache with TTL
this.CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
this.lockOptions = {
retries: { retries: 10, minTimeout: 100, maxTimeout: 300 },
stale: 30000
@@ -47,7 +48,7 @@ class CredentialManager {
if (success) {
// Store metadata separately in file
await this.storeMetadata(key, metadata);
this.cache.set(key, value);
this.cache.set(key, { value, exp: Date.now() + this.CACHE_TTL_MS });
console.log(`[CredentialManager] Stored '${key}' in OS keychain`);
return true;
}
@@ -56,7 +57,7 @@ class CredentialManager {
// Fallback to encrypted file storage
await this.storeInFile(key, value, metadata);
this.cache.set(key, value);
this.cache.set(key, { value, exp: Date.now() + this.CACHE_TTL_MS });
console.log(`[CredentialManager] Stored '${key}' in encrypted file`);
return true;
} catch (error) {
@@ -72,16 +73,20 @@ class CredentialManager {
*/
async retrieve(key) {
try {
// Check cache first
// Check cache first (with TTL expiration)
if (this.cache.has(key)) {
return this.cache.get(key);
const cached = this.cache.get(key);
if (Date.now() < cached.exp) {
return cached.value;
}
this.cache.delete(key);
}
// Try OS keychain first
if (this.useKeychain) {
const value = await keychainManager.retrieve(key);
if (value) {
this.cache.set(key, value);
this.cache.set(key, { value, exp: Date.now() + this.CACHE_TTL_MS });
return value;
}
}
@@ -89,7 +94,7 @@ class CredentialManager {
// Fallback to encrypted file storage
const value = await this.retrieveFromFile(key);
if (value) {
this.cache.set(key, value);
this.cache.set(key, { value, exp: Date.now() + this.CACHE_TTL_MS });
}
return value;
} catch (error) {