Sicherheit: Allowlist anhand des echten Socket-Peers statt X-Forwarded-For

Die Allowlist-Pruefung nutzte extractDebugClientIp, das X-Forwarded-For zuerst
auswertet - ein angreiferkontrollierter Header. Damit konnte ein entfernter Client
auf einem 0.0.0.0-Bind die Allowlist komplett umgehen (X-Forwarded-For: 127.0.0.1 ->
als Loopback gewertet). Die Allowlist bot so keinerlei Schutz; nur das Token blieb.

Fix: Enforcement liest jetzt ausschliesslich req.socket.remoteAddress (getPeerIp),
das vom Kernel aus der TCP-Verbindung gesetzt wird und nicht per Header faelschbar ist.
X-Forwarded-For wird nur noch fuer das Trace-Log (Beobachtbarkeit) verwendet, nie fuer
die Zugriffsentscheidung. Reine, exportierte Matcher-Funktion evaluateClientAllowed.

Tests umgebaut auf den Threat-Model statt den faelschbaren Kanal: Unit-Tests fuer
Loopback/exakt/CIDR/fail-closed + expliziter Forged-XFF-Test (socket.remoteAddress
8.8.8.8 + X-Forwarded-For 127.0.0.1 -> verweigert). Integration: Loopback verbindet,
gespooftes X-Forwarded-For wird ignoriert, Auth bleibt erzwungen, Live-Reload.
This commit is contained in:
Sucukdeluxe
2026-06-19 17:10:11 +02:00
parent c8beaf96ed
commit 95d284f687
3 changed files with 76 additions and 61 deletions
+16 -7
View File
@@ -219,15 +219,23 @@ function matchIpRule(clientIp: string, rule: string): boolean {
return false;
}
function isClientAllowed(clientIp: string): boolean {
export function evaluateClientAllowed(clientIp: string, rules: string[]): boolean {
const client = normalizeIp(clientIp);
if (isLoopbackIp(client) || client === "") {
return true;
}
if (allowlist.length === 0) {
if (rules.length === 0) {
return false;
}
return allowlist.some((rule) => matchIpRule(client, rule));
return rules.some((rule) => matchIpRule(client, rule));
}
export function getPeerIp(req: http.IncomingMessage): string {
return normalizeIp(req.socket?.remoteAddress || "");
}
function isClientAllowed(clientIp: string): boolean {
return evaluateClientAllowed(clientIp, allowlist);
}
function checkAuth(req: http.IncomingMessage): boolean {
@@ -551,15 +559,16 @@ function handleRequest(req: http.IncomingMessage, res: http.ServerResponse): voi
return;
}
const clientIp = extractDebugClientIp(req);
if (!isClientAllowed(clientIp)) {
const peerIp = getPeerIp(req);
if (!isClientAllowed(peerIp)) {
if (traceConfig.enabled && traceConfig.logDebugRequests) {
logTraceEvent("WARN", "debug-http", "Durch Allowlist blockiert", {
clientIp,
peerIp,
forwardedFor: extractDebugClientIp(req),
url: sanitizeRequestUrlForTrace(req.url || "/")
});
}
jsonResponse(res, 403, { error: "Forbidden", reason: "Client-IP nicht in Allowlist", clientIp });
jsonResponse(res, 403, { error: "Forbidden", reason: "Client-IP nicht in Allowlist", clientIp: peerIp });
return;
}