From 48017e895a086966f5e81a602ccbdf3e697217e0 Mon Sep 17 00:00:00 2001 From: Moirtz Wagner Date: Fri, 28 Aug 2026 15:43:43 +0200 Subject: [PATCH] isLocal() haerten: echte Adressvergleiche statt Zeichenketten Die Pruefung zerlegte beide Adressen mit explode(":") und verglich die ersten vier Felder. Bei IPv4 entsteht dabei nur ein Feld, die uebrigen drei sind undefiniert und null == null ist wahr - die Bedingung schrumpfte damit auf REMOTE_ADDR == SERVER_ADDR. Kaeme je ein Reverse Proxy auf denselben Host, haette das jeden Zugriff aus dem Internet als lokal gelten lassen. Neu: - LOCAL_NETWORKS als sichtbare Liste, derzeit 192.168.179.0/24. - ipInNetwork() vergleicht binaer ueber inet_pton, also unabhaengig von der Schreibweise. "2001:0db8:a:b::9" gilt jetzt zurecht als dasselbe /64 wie "2001:db8:a:b::9" - vorher fiel dieser Client durch. - Das IPv6-/64 wird weiterhin aus SERVER_ADDR abgeleitet, da der Provider das Praefix vergibt. - REMOTE_ADDR wird mit filter_var geprueft; "192.168.179.44 evil" galt vorher wegen str_starts_with als lokal. - Auf IPv6 abgebildete IPv4-Adressen (::ffff:192.168.179.44) werden normalisiert. Ein Dual-Stack-Socket meldet LAN-Clients so; bisher fielen sie durch. - $_SESSION["local"] wird in beiden Zweigen gesetzt, nicht nur im positiven. Verhaltensaenderung: Anfragen, deren Absender gleich SERVER_ADDR ist - etwa 127.0.0.1 - gelten nicht mehr automatisch als lokal. Im Repo ruft nichts die Seite ueber HTTP vom Server selbst auf. Wird das doch gebraucht, gehoert das betreffende Netz in LOCAL_NETWORKS. Co-Authored-By: Claude Opus 5 --- helper.php | 78 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 71 insertions(+), 7 deletions(-) diff --git a/helper.php b/helper.php index fedabec..0597058 100644 --- a/helper.php +++ b/helper.php @@ -3,15 +3,79 @@ session_start(); require_once("restricted/mysql.php"); $_SESSION["local"] =false; -function isLocal(){ - $ipv6_prefix = explode(":",$_SERVER['SERVER_ADDR'],5); - $ipv6 = explode(":",$_SERVER['REMOTE_ADDR'],5); - if(($ipv6_prefix[0] == $ipv6[0] && $ipv6_prefix[1] == $ipv6[1] && $ipv6_prefix[2] == $ipv6[2] && $ipv6_prefix[3] == $ipv6[3]) || str_starts_with($_SERVER['REMOTE_ADDR'],"192.168.179.")){ - $_SESSION["local"] =true; - return true; - }else{ +// Netze, die als lokal gelten und damit ohne Anmeldung Zugriff bekommen. +// Bewusst eine sichtbare Liste statt eines Zeichenkettenvergleichs. +const LOCAL_NETWORKS = ["192.168.179.0/24"]; + +/** + * Liegt $ip im Netz $cidr? Verglichen wird binaer (inet_pton), also + * unabhaengig von der Schreibweise: "2001:db8::" und "2001:0db8:0000::" + * sind dieselbe Adresse. + */ +function ipInNetwork($ip, $cidr) +{ + $teile = explode("/", $cidr, 2); + $adresse = @inet_pton($ip); + $netz = @inet_pton($teile[0]); + if ($adresse === false || $netz === false || strlen($adresse) !== strlen($netz)) { + return false; // ungueltig oder verschiedene Adressfamilien + } + $bits = isset($teile[1]) ? intval($teile[1]) : strlen($adresse) * 8; + $ganzeBytes = intdiv($bits, 8); + $restBits = $bits % 8; + if ($ganzeBytes > 0 && strncmp($adresse, $netz, $ganzeBytes) !== 0) { return false; } + if ($restBits === 0) { + return true; + } + $maske = chr((0xFF << (8 - $restBits)) & 0xFF); + return ($adresse[$ganzeBytes] & $maske) === ($netz[$ganzeBytes] & $maske); +} + +/** + * Kommt die Anfrage aus dem Heimnetz? Nur dann gilt sie ohne Anmeldung als + * berechtigt. + * + * REMOTE_ADDR stammt aus der TCP-Verbindung und ist nicht faelschbar. + * Weiterleitungs-Header wie X-Forwarded-For duerfen hier bewusst NICHT + * herangezogen werden - sie kann jeder Aufrufer frei setzen. + */ +function isLocal() +{ + $_SESSION["local"] = false; + $remote = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : ''; + $server = isset($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : ''; + + if (filter_var($remote, FILTER_VALIDATE_IP) === false) { + return false; + } + + // Auf IPv6 abgebildete IPv4-Adressen (::ffff:192.168.179.44) auf ihre + // IPv4-Form zurueckfuehren. Ein Server mit Dual-Stack-Socket meldet + // LAN-Clients so - ohne diesen Schritt faellt das Heimnetz durch. + if (preg_match('/^::ffff:(\d{1,3}(?:\.\d{1,3}){3})$/i', $remote, $treffer)) { + $remote = $treffer[1]; + } + + $lokal = false; + foreach (LOCAL_NETWORKS as $netz) { + if (ipInNetwork($remote, $netz)) { + $lokal = true; + break; + } + } + + // Gleiches IPv6-/64 wie der Server. Das Praefix vergibt der Provider und + // es kann sich aendern, deshalb aus SERVER_ADDR abgeleitet statt fest. + if (!$lokal + && filter_var($remote, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false + && filter_var($server, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false) { + $lokal = substr(inet_pton($remote), 0, 8) === substr(inet_pton($server), 0, 8); + } + + $_SESSION["local"] = $lokal; + return $lokal; } /**