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:
@@ -32,7 +32,16 @@ module.exports = function(ctx) {
|
||||
const timestamps = req.query.timestamps !== 'false';
|
||||
|
||||
const container = ctx.docker.client.getContainer(containerId);
|
||||
const info = await container.inspect();
|
||||
let info;
|
||||
try {
|
||||
info = await container.inspect();
|
||||
} catch (err) {
|
||||
if (err.statusCode === 404 || (err.message && err.message.includes('no such container'))) {
|
||||
const { NotFoundError } = require('../errors');
|
||||
throw new NotFoundError(`Container ${containerId}`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
const containerName = info.Name.replace(/^\//, '');
|
||||
|
||||
const logs = await container.logs({
|
||||
@@ -74,6 +83,15 @@ module.exports = function(ctx) {
|
||||
router.get('/logs/stream/:id', ctx.asyncHandler(async (req, res) => {
|
||||
const containerId = req.params.id;
|
||||
const container = ctx.docker.client.getContainer(containerId);
|
||||
try {
|
||||
await container.inspect();
|
||||
} catch (err) {
|
||||
if (err.statusCode === 404 || (err.message && err.message.includes('no such container'))) {
|
||||
const { NotFoundError } = require('../errors');
|
||||
throw new NotFoundError(`Container ${containerId}`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
@@ -132,20 +150,32 @@ module.exports = function(ctx) {
|
||||
const allowedPaths = platformPaths.allowedLogPaths;
|
||||
|
||||
const normalizedPath = path.normalize(logPath);
|
||||
const isAllowed = allowedPaths.some(allowed =>
|
||||
normalizedPath.startsWith(path.normalize(allowed))
|
||||
);
|
||||
|
||||
// Resolve symlinks to prevent symlink-based traversal
|
||||
let resolvedPath;
|
||||
try {
|
||||
resolvedPath = await fsp.realpath(normalizedPath);
|
||||
} catch {
|
||||
const { NotFoundError } = require('../errors');
|
||||
throw new NotFoundError('Log file');
|
||||
}
|
||||
|
||||
// Check path against allowed roots with separator boundary
|
||||
const isAllowed = allowedPaths.some(allowed => {
|
||||
const normalizedAllowed = path.normalize(allowed);
|
||||
return resolvedPath === normalizedAllowed || resolvedPath.startsWith(normalizedAllowed + path.sep);
|
||||
});
|
||||
|
||||
if (!isAllowed) {
|
||||
return ctx.errorResponse(res, 403, 'Access to this log path is not allowed');
|
||||
}
|
||||
|
||||
if (!await exists(normalizedPath)) {
|
||||
if (!await exists(resolvedPath)) {
|
||||
const { NotFoundError } = require('../errors');
|
||||
throw new NotFoundError('Log file');
|
||||
}
|
||||
|
||||
const fileContent = await fsp.readFile(normalizedPath, 'utf8');
|
||||
const fileContent = await fsp.readFile(resolvedPath, 'utf8');
|
||||
const lines = fileContent.split('\n').filter(line => line.trim());
|
||||
const tailLines = lines.slice(-tail);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user