Security: Versions-Downgrade-Schutz im Agent-Update-Mechanismus

checkin() und downloadSetup() liefern jetzt nie eine ältere Version aus,
als der Agent laut DB/eigenem Report bereits installiert hat (clampToNotOlder).
Schützt gegen genau den Bug von heute (kaputte AGENT_VERSION env-Var führte
zu echtem Downgrade auf v2.0.0 bei 17 Agents) — selbst bei künftigen
Konfigurationsfehlern kann der Server keinen Downgrade mehr anstoßen.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-26 14:16:55 +02:00
parent 33e0b73f7f
commit 07fa8d6020

View File

@@ -12,6 +12,24 @@ const crypto = require('crypto');
// Downgrade-Bug für Agents ohne Patch-Gruppe oder ohne ?hostname-Parameter.) // Downgrade-Bug für Agents ohne Patch-Gruppe oder ohne ?hostname-Parameter.)
const FALLBACK_AGENT_VERSION = '2.8.0'; const FALLBACK_AGENT_VERSION = '2.8.0';
// Vergleicht zwei "x.y.z"-Versionsstrings. >0 wenn a>b, <0 wenn a<b, 0 wenn gleich.
function compareVersions(a, b) {
const pa = String(a || '0').split('.').map(n => parseInt(n, 10) || 0);
const pb = String(b || '0').split('.').map(n => parseInt(n, 10) || 0);
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
const diff = (pa[i] || 0) - (pb[i] || 0);
if (diff !== 0) return diff;
}
return 0;
}
// Nie eine ältere Version als die aktuell installierte vorschlagen/ausliefern — verhindert
// Downgrades durch fehlerhafte ENV-Konfiguration, falsch gesetzte Gruppen-Zielversionen o.ä.
function clampToNotOlder(targetVersion, currentVersion) {
if (!currentVersion) return targetVersion;
return compareVersions(targetVersion, currentVersion) < 0 ? currentVersion : targetVersion;
}
// POST /api/monitoring/enroll — Agent tauscht geteilten Bootstrap-Key gegen individuellen Per-Device-Key. // POST /api/monitoring/enroll — Agent tauscht geteilten Bootstrap-Key gegen individuellen Per-Device-Key.
// Idempotent: ein bereits enrollter Agent bekommt seinen bestehenden Key einfach erneut zurück. // Idempotent: ein bereits enrollter Agent bekommt seinen bestehenden Key einfach erneut zurück.
const enroll = asyncHandler(async (req, res) => { const enroll = asyncHandler(async (req, res) => {
@@ -143,6 +161,8 @@ const checkin = asyncHandler(async (req, res) => {
if (groupRow?.target_agent_version) targetVersion = groupRow.target_agent_version; if (groupRow?.target_agent_version) targetVersion = groupRow.target_agent_version;
} catch { /* kein Gruppe zugewiesen → global */ } } catch { /* kein Gruppe zugewiesen → global */ }
targetVersion = clampToNotOlder(targetVersion, req.body.agent_version);
const announcements = getForAgent(agent.id); const announcements = getForAgent(agent.id);
res.json({ res.json({
@@ -254,7 +274,7 @@ const downloadSetup = asyncHandler(async (req, res) => {
if (hostname) { if (hostname) {
try { try {
const db = getDatabase(); const db = getDatabase();
const agent = db.prepare('SELECT id FROM monitoring_agents WHERE hostname = ?').get(hostname); const agent = db.prepare('SELECT id, agent_version FROM monitoring_agents WHERE hostname = ?').get(hostname);
const groupRow = agent && db.prepare(` const groupRow = agent && db.prepare(`
SELECT pg.target_agent_version SELECT pg.target_agent_version
FROM patch_agent_groups pag FROM patch_agent_groups pag
@@ -264,6 +284,7 @@ const downloadSetup = asyncHandler(async (req, res) => {
LIMIT 1 LIMIT 1
`).get(agent.id); `).get(agent.id);
if (groupRow?.target_agent_version) version = groupRow.target_agent_version; if (groupRow?.target_agent_version) version = groupRow.target_agent_version;
version = clampToNotOlder(version, agent?.agent_version);
} catch { /* kein Gruppe zugewiesen → global */ } } catch { /* kein Gruppe zugewiesen → global */ }
} }