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:
@@ -230,9 +230,19 @@ function _httpFetch(url, opts = {}, timeoutMs = TIMEOUTS.HTTP_DEFAULT) {
|
||||
if (opts.body) {
|
||||
options.headers['Content-Length'] = Buffer.byteLength(opts.body);
|
||||
}
|
||||
const MAX_RESPONSE_SIZE = 10 * 1024 * 1024; // 10MB
|
||||
const req = http.request(options, (res) => {
|
||||
let data = '';
|
||||
res.on('data', chunk => data += chunk);
|
||||
let size = 0;
|
||||
res.on('data', chunk => {
|
||||
size += chunk.length;
|
||||
if (size > MAX_RESPONSE_SIZE) {
|
||||
res.destroy();
|
||||
reject(new Error(`Response from ${url} exceeded ${MAX_RESPONSE_SIZE} bytes`));
|
||||
return;
|
||||
}
|
||||
data += chunk;
|
||||
});
|
||||
res.on('end', () => {
|
||||
resolve({
|
||||
ok: res.statusCode >= 200 && res.statusCode < 300,
|
||||
@@ -317,12 +327,11 @@ async function readConfig() {
|
||||
return readJsonFile(CONFIG_FILE, {});
|
||||
}
|
||||
|
||||
/** Save config.json (merges with existing) */
|
||||
/** Save config.json (merges with existing, atomic with locking) */
|
||||
async function saveConfig(updates) {
|
||||
const config = await readConfig();
|
||||
Object.assign(config, updates);
|
||||
await writeJsonFile(CONFIG_FILE, config);
|
||||
return config;
|
||||
return await configStateManager.update(config => {
|
||||
return Object.assign(config, updates);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -676,17 +685,11 @@ async function loadTotpConfig() {
|
||||
try {
|
||||
if (await exists(TOTP_CONFIG_FILE)) {
|
||||
const data = await fsp.readFile(TOTP_CONFIG_FILE, 'utf8');
|
||||
Object.assign(totpConfig, JSON.parse(data));
|
||||
const loaded = JSON.parse(data);
|
||||
// Never load secret from file — it belongs only in credential-manager
|
||||
delete loaded.secret;
|
||||
Object.assign(totpConfig, loaded);
|
||||
log.info('config', 'TOTP config loaded', { enabled: totpConfig.enabled });
|
||||
|
||||
// Auto-restore: if config has a backup secret but credential manager lost it, re-store it
|
||||
if (totpConfig.secret && totpConfig.isSetUp) {
|
||||
const existing = await credentialManager.retrieve('totp.secret');
|
||||
if (!existing) {
|
||||
await credentialManager.store('totp.secret', totpConfig.secret);
|
||||
log.info('config', 'TOTP secret auto-restored from config backup');
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
await logError('loadTotpConfig', e);
|
||||
|
||||
Reference in New Issue
Block a user