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 <noreply@anthropic.com>
This commit is contained in:
2026-08-28 15:43:43 +02:00
co-authored by Claude Opus 5
parent 768520cedd
commit 48017e895a
+71 -7
View File
@@ -3,15 +3,79 @@ session_start();
require_once("restricted/mysql.php"); require_once("restricted/mysql.php");
$_SESSION["local"] =false; $_SESSION["local"] =false;
function isLocal(){ // Netze, die als lokal gelten und damit ohne Anmeldung Zugriff bekommen.
$ipv6_prefix = explode(":",$_SERVER['SERVER_ADDR'],5); // Bewusst eine sichtbare Liste statt eines Zeichenkettenvergleichs.
$ipv6 = explode(":",$_SERVER['REMOTE_ADDR'],5); const LOCAL_NETWORKS = ["192.168.179.0/24"];
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; * Liegt $ip im Netz $cidr? Verglichen wird binaer (inet_pton), also
}else{ * 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; 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;
} }
/** /**