PHP: Wie ermittelt man die IP-Adresse des Clients?

Die IP-Adresse des Besuchers braucht man für Statistiken, für Sperrlisten oder für eine Begrenzung der Anfragen pro Minute. Die Antwort scheint einfach zu sein, es gibt dafür aber genau eine verlässliche Quelle — und einige, die nur so aussehen.

Die verlässliche Quelle

<?php
  echo $_SERVER['REMOTE_ADDR'];
?>

REMOTE_ADDR ist die Adresse, von der die TCP-Verbindung tatsächlich kommt. Diesen Wert setzt der Webserver, nicht der Client. Er lässt sich also nicht fälschen.

Solange kein Proxy zwischen dir und dem Besucher steht, bist du damit fertig.

Das Problem mit dem Proxy

Steht ein Reverse Proxy, ein Load Balancer oder ein CDN davor, dann kommt die Verbindung von diesem Proxy. REMOTE_ADDR zeigt dann nicht mehr auf den Besucher.

Ich habe das nachgestellt: curl → nginx als Proxy → nginx als Webserver → PHP-FPM. Einmal direkt abgefragt und einmal über den Proxy.

Direkt, ohne Proxy:

REMOTE_ADDR            172.21.0.1
HTTP_X_FORWARDED_FOR   (nicht gesetzt)

Über den Proxy:

REMOTE_ADDR            172.21.0.4     <- der Proxy
HTTP_X_FORWARDED_FOR   172.21.0.1     <- der echte Client

Der Proxy hat also einen zusätzlichen Header gesetzt, in dem die ursprüngliche Adresse steht. Und genau deshalb findet man im Netz überall Codeschnipsel wie diesen:

<?php
// Achtung, diese verbreitete Variante ist gefährlich!
function clientIp(): string {
  if (!empty($_SERVER['HTTP_CLIENT_IP'])) {
    return $_SERVER['HTTP_CLIENT_IP'];
  }
  if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    return explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])[0];
  }
  return $_SERVER['REMOTE_ADDR'];
}
?>

Warum dieser Code gefährlich ist

HTTP-Header kommen vom Client. Jeder kann sie setzen. Ich habe dieselbe Seite noch einmal abgefragt, diesmal mit erfundenen Headern:

user@pc:~$ curl -H 'X-Forwarded-For: 1.2.3.4' -H 'Client-IP: 9.9.9.9' http://localhost:8080/ip.php

Das Ergebnis über den Proxy:

REMOTE_ADDR            172.21.0.4
HTTP_X_FORWARDED_FOR   1.2.3.4, 172.21.0.1
HTTP_CLIENT_IP         9.9.9.9

Zwei Dinge fallen auf.

Erstens steht in HTTP_CLIENT_IP einfach der Wert, den ich mitgeschickt habe. Diesen Header hat kein Proxy gesetzt. Die obige Funktion prüft ihn aber als erstes und würde 9.9.9.9 zurückgeben. Ein Angreifer bestimmt damit vollständig, was in deinen Logs und Sperrlisten landet.

Zweitens, und das ist der subtilere Punkt: In X-Forwarded-For stehen jetzt zwei Adressen.

1.2.3.4, 172.21.0.1
^^^^^^^  ^^^^^^^^^^
gefälscht  echt

Der Proxy hat meinen erfundenen Wert nicht ersetzt, sondern die tatsächliche Absenderadresse hinten angehängt. Das ist das vorgesehene Verhalten, denn der Header soll die gesamte Kette abbilden. Für dich heißt das aber: Der gefälschte Wert steht vorne. Und explode(',', $xff)[0] liest genau diesen.

Ohne Proxy ist es noch deutlicher:

REMOTE_ADDR            172.21.0.1     <- weiterhin korrekt
HTTP_X_FORWARDED_FOR   1.2.3.4        <- frei erfunden

Wie man es richtig macht

Die Regel ist einfach: Vertraue einem Header nur dann, wenn du weißt, wer ihn gesetzt hat.

Steht kein Proxy davor, verwendest du ausschließlich REMOTE_ADDR. Punkt.

Steht dein eigener Proxy davor, dann prüfst du erst, ob die Verbindung tatsächlich von diesem Proxy kommt, und wertest den Header erst dann aus:

<?php
function clientIp(): string {
  $proxies = ['172.21.0.4'];   // die Adressen deiner eigenen Proxies

  $remote = $_SERVER['REMOTE_ADDR'];

  if (!in_array($remote, $proxies, true)) {
    // Die Verbindung kommt nicht von unserem Proxy,
    // also ist REMOTE_ADDR bereits der Client.
    return $remote;
  }

  if (empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
    return $remote;
  }

  $kette = array_map('trim', explode(',', $_SERVER['HTTP_X_FORWARDED_FOR']));

  // Der letzte Eintrag stammt von unserem Proxy und ist damit
  // der einzige, den wir nicht selbst geschenkt bekommen haben.
  return end($kette);
}
?>

Wichtig ist der letzte Punkt: Du nimmst den letzten Eintrag der Liste, nicht den ersten. Alles was davor steht, hat der Client geliefert.

Bei mehreren hintereinandergeschalteten Proxies musst du entsprechend so viele Einträge von hinten überspringen, wie du eigene Proxies hast. Und wenn du ein CDN einsetzt, sieh in dessen Dokumentation nach — die meisten setzen einen eigenen, nicht ergänzbaren Header.

Und was ist mit IPv6?

REMOTE_ADDR liefert je nach Verbindung eine IPv4- oder eine IPv6-Adresse. Wenn du die Adresse in einer Datenbank ablegst, plane das Feld also groß genug ein. Eine IPv6-Adresse kann in Textform bis zu 45 Zeichen lang werden.

Zusammenfassung

  • Ohne Proxy: nur REMOTE_ADDR, alles andere ist fälschbar.
  • HTTP_CLIENT_IP niemals verwenden, den setzt in der Praxis nur der Angreifer.
  • Mit eigenem Proxy: erst prüfen ob REMOTE_ADDR der Proxy ist.
  • Aus X-Forwarded-For den letzten Eintrag nehmen, nicht den ersten.

Hinweis zu Netcup (Werbung)

Der deutsche Hoster Netcup bietet unter anderem günstige und zugleich leistungsstarke Webhosting Pakete, KVM-basierte Root Server und dezidierte Server an. Mit unseren Gutscheincodes kannst du noch mehr Geld sparen (6€ bei deiner ersten Bestellung, 30% Rabatt auf alle KVM-basierten Root Server, ...).