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:
@@ -8,6 +8,17 @@ const { exists } = require('../fs-helpers');
|
||||
module.exports = function(ctx) {
|
||||
const router = express.Router();
|
||||
|
||||
/** Validate that a server IP is in the configured DNS servers list */
|
||||
function validateDnsServer(server) {
|
||||
const serverIp = server.includes(':') ? server.split(':')[0] : server;
|
||||
if (!validatorLib.isIP(serverIp)) return null;
|
||||
const configuredIps = Object.values(ctx.siteConfig.dnsServers || {}).map(s => s.ip).filter(Boolean);
|
||||
// Also allow the default dnsServerIp
|
||||
if (ctx.siteConfig.dnsServerIp) configuredIps.push(ctx.siteConfig.dnsServerIp);
|
||||
if (!configuredIps.includes(serverIp)) return null;
|
||||
return serverIp;
|
||||
}
|
||||
|
||||
// DELETE /record — Delete a DNS record from Technitium
|
||||
router.delete('/record', ctx.asyncHandler(async (req, res) => {
|
||||
const { domain, type, token, server, ipAddress } = req.query;
|
||||
@@ -33,9 +44,9 @@ module.exports = function(ctx) {
|
||||
return ctx.errorResponse(res, 400, '[DC-210] Invalid IP address');
|
||||
}
|
||||
|
||||
// Validate server if provided
|
||||
if (server && !validatorLib.isIP(server)) {
|
||||
return ctx.errorResponse(res, 400, 'Invalid DNS server address');
|
||||
// Validate server against configured DNS servers
|
||||
if (server && !validateDnsServer(server)) {
|
||||
return ctx.errorResponse(res, 400, 'Server must be a configured DNS server');
|
||||
}
|
||||
|
||||
// Default to dns1 LAN IP, allow override
|
||||
@@ -85,9 +96,9 @@ module.exports = function(ctx) {
|
||||
}
|
||||
}
|
||||
|
||||
// Validate server IP if provided
|
||||
if (server && !validatorLib.isIP(server)) {
|
||||
return ctx.errorResponse(res, 400, 'Invalid DNS server address');
|
||||
// Validate server against configured DNS servers
|
||||
if (server && !validateDnsServer(server)) {
|
||||
return ctx.errorResponse(res, 400, 'Server must be a configured DNS server');
|
||||
}
|
||||
|
||||
// Default to dns1 LAN IP since Docker container can't access Tailscale network
|
||||
@@ -131,9 +142,9 @@ module.exports = function(ctx) {
|
||||
return ctx.errorResponse(res, 400, '[DC-301] Invalid domain format');
|
||||
}
|
||||
|
||||
// Validate server if provided
|
||||
if (server && !validatorLib.isIP(server)) {
|
||||
return ctx.errorResponse(res, 400, 'Invalid DNS server address');
|
||||
// Validate server against configured DNS servers
|
||||
if (server && !validateDnsServer(server)) {
|
||||
return ctx.errorResponse(res, 400, 'Server must be a configured DNS server');
|
||||
}
|
||||
|
||||
const dnsServer = server || ctx.siteConfig.dnsServerIp;
|
||||
@@ -169,17 +180,17 @@ module.exports = function(ctx) {
|
||||
return ctx.errorResponse(res, 400, 'server is required');
|
||||
}
|
||||
|
||||
// Validate server is an IP address or hostname to prevent SSRF
|
||||
const serverClean = server.includes(':') ? server.split(':')[0] : server;
|
||||
if (!validatorLib.isIP(serverClean) && !validatorLib.isFQDN(serverClean)) {
|
||||
return ctx.errorResponse(res, 400, 'Invalid DNS server address');
|
||||
// Validate server against configured DNS servers
|
||||
const serverIp = validateDnsServer(server);
|
||||
if (!serverIp) {
|
||||
return ctx.errorResponse(res, 400, 'Server must be a configured DNS server');
|
||||
}
|
||||
|
||||
const logLimit = Math.min(parseInt(limit) || 25, 1000);
|
||||
const dnsPort = ctx.siteConfig.dnsServerPort || '5380';
|
||||
|
||||
try {
|
||||
// Auto-authenticate using stored read-only credentials for log access
|
||||
const serverIp = server.includes(':') ? server.split(':')[0] : server;
|
||||
const authResult = await ctx.dns.getTokenForServer(serverIp, 'readonly');
|
||||
if (!authResult.success) {
|
||||
return ctx.errorResponse(res, 401, 'DNS auto-authentication failed. Ensure credentials are configured via the DNS panel.');
|
||||
@@ -187,7 +198,7 @@ module.exports = function(ctx) {
|
||||
const effectiveToken = authResult.token;
|
||||
|
||||
// Try to get available log files first
|
||||
const listUrl = `http://${server}/api/logs/list?token=${encodeURIComponent(effectiveToken)}`;
|
||||
const listUrl = `http://${serverIp}:${dnsPort}/api/logs/list?token=${encodeURIComponent(effectiveToken)}`;
|
||||
const listResponse = await ctx.fetchT(listUrl, { method: 'GET', headers: { 'Accept': 'application/json' } });
|
||||
|
||||
let logFileName = new Date().toISOString().split('T')[0]; // Default to today
|
||||
@@ -201,7 +212,7 @@ module.exports = function(ctx) {
|
||||
}
|
||||
|
||||
// Technitium logs/download endpoint - returns plain text logs
|
||||
const technitiumUrl = `http://${server}/api/logs/download?token=${encodeURIComponent(effectiveToken)}&fileName=${logFileName}`;
|
||||
const technitiumUrl = `http://${serverIp}:${dnsPort}/api/logs/download?token=${encodeURIComponent(effectiveToken)}&fileName=${logFileName}`;
|
||||
ctx.log.info('dns', 'Fetching DNS logs', { server, logFileName });
|
||||
|
||||
const response = await ctx.fetchT(technitiumUrl, {
|
||||
@@ -400,8 +411,8 @@ module.exports = function(ctx) {
|
||||
return ctx.errorResponse(res, 400, 'Username contains invalid characters');
|
||||
}
|
||||
|
||||
if (server && !validatorLib.isIP(server)) {
|
||||
return ctx.errorResponse(res, 400, 'Invalid DNS server address');
|
||||
if (server && !validateDnsServer(server)) {
|
||||
return ctx.errorResponse(res, 400, 'Server must be a configured DNS server');
|
||||
}
|
||||
|
||||
const testResult = await ctx.dns.refresh(username, password, server || ctx.siteConfig.dnsServerIp);
|
||||
@@ -499,13 +510,19 @@ module.exports = function(ctx) {
|
||||
return ctx.errorResponse(res, 400, 'Server IP required');
|
||||
}
|
||||
|
||||
const serverIp = validateDnsServer(server);
|
||||
if (!serverIp) {
|
||||
return ctx.errorResponse(res, 400, 'Server must be a configured DNS server');
|
||||
}
|
||||
|
||||
// Authenticate with admin credentials for update check
|
||||
const tokenResult = await ctx.dns.getTokenForServer(server, 'admin');
|
||||
const tokenResult = await ctx.dns.getTokenForServer(serverIp, 'admin');
|
||||
if (!tokenResult.success) {
|
||||
return ctx.errorResponse(res, 401, 'DNS authentication failed. Ensure credentials are configured.');
|
||||
}
|
||||
|
||||
const url = `http://${server}:5380/api/user/checkForUpdate?token=${encodeURIComponent(tokenResult.token)}`;
|
||||
const dnsPort = ctx.siteConfig.dnsServerPort || '5380';
|
||||
const url = `http://${serverIp}:${dnsPort}/api/user/checkForUpdate?token=${encodeURIComponent(tokenResult.token)}`;
|
||||
ctx.log.info('dns', 'Checking DNS update', { server });
|
||||
|
||||
const response = await ctx.fetchT(url, {
|
||||
@@ -554,15 +571,21 @@ module.exports = function(ctx) {
|
||||
return ctx.errorResponse(res, 400, 'Server IP required');
|
||||
}
|
||||
|
||||
const serverIp = validateDnsServer(server);
|
||||
if (!serverIp) {
|
||||
return ctx.errorResponse(res, 400, 'Server must be a configured DNS server');
|
||||
}
|
||||
|
||||
// Authenticate with admin credentials for update operations
|
||||
const tokenResult = await ctx.dns.getTokenForServer(server, 'admin');
|
||||
const tokenResult = await ctx.dns.getTokenForServer(serverIp, 'admin');
|
||||
if (!tokenResult.success) {
|
||||
return ctx.errorResponse(res, 401, 'DNS authentication failed. Ensure credentials are configured.');
|
||||
}
|
||||
|
||||
const dnsPort = ctx.siteConfig.dnsServerPort || '5380';
|
||||
// Check if update is available
|
||||
const checkResponse = await ctx.fetchT(
|
||||
`http://${server}:5380/api/user/checkForUpdate?token=${encodeURIComponent(tokenResult.token)}`,
|
||||
`http://${serverIp}:${dnsPort}/api/user/checkForUpdate?token=${encodeURIComponent(tokenResult.token)}`,
|
||||
{ method: 'GET', headers: { 'Accept': 'application/json' } }
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user