['trusted_proxies' => ['10.0.0.0/8', '192.168.1.5', 'fd00::/8']]])); require_once dirname(__DIR__) . '/includes/bootstrap.php'; $failures = []; $check = function (bool $ok, string $what) use (&$failures): void { if (!$ok) $failures[] = $what; }; // CIDR-Vergleich $check(ip_in_list('10.1.2.3', ['10.0.0.0/8']), '10.1.2.3 in 10.0.0.0/8'); $check(!ip_in_list('11.1.2.3', ['10.0.0.0/8']), '11.1.2.3 nicht in 10.0.0.0/8'); $check(ip_in_list('192.168.1.5', ['192.168.1.5']), 'Einzeladresse'); $check(!ip_in_list('192.168.1.6', ['192.168.1.5']), 'andere Einzeladresse'); $check(ip_in_list('172.17.0.3', ['172.16.0.0/12']), '/12-Bereich'); $check(!ip_in_list('172.32.0.1', ['172.16.0.0/12']), 'außerhalb /12'); $check(ip_in_list('fd00::1234', ['fd00::/8']), 'IPv6 im Bereich'); $check(!ip_in_list('2001:db8::1', ['fd00::/8']), 'IPv6 außerhalb'); $check(!ip_in_list('10.0.0.1', ['fd00::/8']), 'IPv4 gegen IPv6-Eintrag'); $check(!ip_in_list('kaputt', ['10.0.0.0/8']), 'ungültige IP'); // Direkter Zugriff: X-Forwarded-For wird ignoriert (sonst könnte jeder die Sperre umgehen) $_SERVER['REMOTE_ADDR'] = '203.0.113.9'; $_SERVER['HTTP_X_FORWARDED_FOR'] = '1.2.3.4'; $_SERVER['HTTP_X_FORWARDED_PROTO'] = 'https'; $check(client_ip() === '203.0.113.9', 'Ohne vertrauenswürdigen Proxy zählt REMOTE_ADDR: ' . client_ip()); $check(!request_is_https(), 'Ohne vertrauenswürdigen Proxy wird X-Forwarded-Proto ignoriert'); // Über vertrauenswürdigen Proxy $_SERVER['REMOTE_ADDR'] = '10.0.0.7'; $_SERVER['HTTP_X_FORWARDED_FOR'] = '198.51.100.20'; $check(client_ip() === '198.51.100.20', 'Proxy: Client-IP aus X-Forwarded-For: ' . client_ip()); $check(request_is_https(), 'Proxy: X-Forwarded-Proto https wird erkannt'); // Der Angreifer hängt eine eigene Adresse vorne an: maßgeblich ist die letzte nicht vertrauenswürdige von rechts $_SERVER['HTTP_X_FORWARDED_FOR'] = '1.1.1.1, 198.51.100.20, 10.0.0.3'; $check(client_ip() === '198.51.100.20', 'Gefälschte Adresse vorne wird ignoriert: ' . client_ip()); $_SERVER['HTTP_X_FORWARDED_FOR'] = 'kein-ip-wert'; $check(client_ip() === '10.0.0.7', 'Ungültiger Header fällt auf REMOTE_ADDR zurück'); // Login-Zähler: nach Erfolg zurücksetzbar $_SERVER['REMOTE_ADDR'] = '203.0.113.9'; unset($_SERVER['HTTP_X_FORWARDED_FOR']); for ($i = 0; $i < 5; $i++) $check(rate_limit_check('login_email', 'a@example.org', 5, 15), "Versuch {$i} erlaubt"); $check(!rate_limit_check('login_email', 'a@example.org', 5, 15), 'sechster Versuch gesperrt'); rate_limit_reset('login_email', 'a@example.org'); $check(rate_limit_check('login_email', 'a@example.org', 5, 15), 'nach dem Zurücksetzen wieder erlaubt'); $check(!rate_limit_check('login_email', 'b@example.org', 0, 15), 'anderer Schlüssel unabhängig'); // Abteilungsprüfung für Mitarbeiter (verhindert Zugriff auf Tickets/Anhänge fremder Abteilungen) $pdo = get_db(); $pdo->exec("INSERT INTO departments (name, email, is_default) VALUES ('Zweite Abteilung', 'zweite@example.org', 0)"); $deptA = (int) $pdo->query("SELECT id FROM departments ORDER BY id LIMIT 1")->fetchColumn(); $deptB = (int) $pdo->query("SELECT id FROM departments ORDER BY id DESC LIMIT 1")->fetchColumn(); $check($deptA !== $deptB, "zwei Abteilungen angelegt"); $admin = ["id" => 1, "role" => "admin", "department_id" => null]; $agentA = ["id" => 2, "role" => "agent", "department_id" => $deptA]; $agentNone = ["id" => 3, "role" => "agent", "department_id" => null]; $check(staff_can_access_department($admin, $deptB), "Admin darf jede Abteilung"); $check(staff_can_access_department($agentA, $deptA), "Agent darf eigene Abteilung"); $check(!staff_can_access_department($agentA, $deptB), "Agent darf fremde Abteilung nicht"); $check(!staff_can_access_department($agentNone, $deptA), "Agent ohne Abteilung darf nichts"); if ($failures) { fwrite(STDERR, "FEHLER:\n- " . implode("\n- ", $failures) . "\n"); exit(1); } echo "Security helpers checks passed.\n";