Ce qu’il faut savoir avant de manipuler une IP en PHP
- `$_SERVER['REMOTE_ADDR']` donne l’adresse du pair direct, pas toujours celle du navigateur final.
- Les en-têtes comme `X-Forwarded-For` ne sont fiables que si vous passez par des proxies que vous contrôlez.
- `filter_var(..., FILTER_VALIDATE_IP)` est la validation de base la plus propre pour IPv4 et IPv6.
- `inet_pton()` et `inet_ntop()` sont les bons outils dès que l’IPv6 entre en jeu.
- Pour la conformité et les logs, une IP doit être traitée comme une donnée sensible, pas comme un identifiant absolu.
Ce que PHP lit vraiment dans une connexion
Quand je récupère une adresse IP côté serveur, je pars d’une idée simple : PHP ne voit pas “l’utilisateur”, il voit d’abord la connexion réseau qui arrive jusqu’à lui. Dans le cas le plus simple, cette valeur se trouve dans `$_SERVER['REMOTE_ADDR']`. C’est généralement la bonne base, mais seulement si la requête arrive directement sur votre application.
Le piège classique, c’est le reverse proxy, le CDN ou l’équilibreur de charge. Dans ce cas, `REMOTE_ADDR` décrit souvent le dernier relais réseau, pas le visiteur final. Les autres champs de `$_SERVER` existent aussi, mais ils n’ont pas le même niveau de confiance ni le même usage.
| Source | Ce qu’elle représente | Quand l’utiliser | Limite |
|---|---|---|---|
| `REMOTE_ADDR` | L’adresse du pair TCP direct | Connexion directe, base de confiance réseau | Derrière un proxy, ce n’est pas forcément le client final |
| `HTTP_X_FORWARDED_FOR` | Chaîne d’adresses propagée par les proxies | Infrastructure maîtrisée, proxies de confiance | Falsifiable si elle est prise en compte sans contrôle |
| `REMOTE_HOST` | Nom résolu par DNS inverse | Diagnostic, support, administration | Dépend de la configuration serveur et du DNS |
| `gethostbyaddr()` | Résolution inverse à la demande | Rapports ou outils d’admin ponctuels | Peut échouer ou retourner l’adresse elle-même |
En pratique, je considère donc `REMOTE_ADDR` comme la source de départ, pas comme une vérité absolue. Et si l’environnement n’est pas clair, mieux vaut retourner `null` que d’en déduire une IP “probablement correcte”. La suite logique, c’est justement de savoir traiter le cas des proxies sans se faire piéger par un en-tête inventé.

Récupérer l’IP du visiteur derrière un proxy
Derrière un proxy, je ne fais jamais confiance à un en-tête juste parce qu’il existe. Je le lis uniquement si l’adresse source de la connexion appartient à une liste de proxys que je contrôle. C’est le point qui évite la majorité des erreurs de sécurité dans les scripts PHP qui “récupèrent l’IP du client”.
Le principe est le suivant : si la requête vient d’un relais connu, alors les en-têtes comme `X-Forwarded-For` ou, selon votre pile, `Forwarded`, peuvent contenir la chaîne des sauts réseau. Si la requête ne vient pas d’un relais connu, j’ignore ces en-têtes. C’est une règle simple, mais elle change tout.
= 0; $i--) {
$candidate = trim($chain[$i]);
if (filter_var($candidate, FILTER_VALIDATE_IP) !== false) {
return $candidate;
}
}
return $remoteAddr;
}
Je pars ici d’un modèle volontairement sobre : il est facile à relire, et surtout il force la logique de confiance. Dans un environnement plus complexe, je peux affiner la liste des relais autorisés, mais je garde la même règle : seuls les proxies connus peuvent enrichir l’information. Sans cette discipline, l’IP devient un champ facilement manipulable. Maintenant qu’on sait quelle valeur lire, il faut la valider proprement avant d’aller plus loin.
Valider l’adresse avant de la stocker ou de l’exploiter
Pour valider une IP, je privilégie `filter_var()` avec `FILTER_VALIDATE_IP`. C’est plus lisible qu’une regex maison, et surtout plus robuste dès qu’on gère à la fois IPv4 et IPv6. Une chaîne comme `2001:db8::1` est parfaitement valide, alors qu’une approche artisanale finit souvent par casser sur la compression IPv6 ou les notations mixtes.
Le vrai intérêt des flags apparaît quand vous devez filtrer davantage que la simple validité syntaxique. Si votre application n’accepte que des IP publiques, j’ajoute souvent `FILTER_FLAG_NO_PRIV_RANGE` et `FILTER_FLAG_NO_RES_RANGE`. Cela permet d’écarter des plages privées comme `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, mais aussi des plages réservées ou non routables.
Quand je dois être encore plus strict, j’utilise aussi `FILTER_FLAG_IPV4` ou `FILTER_FLAG_IPV6` pour forcer un protocole précis. Dans les versions récentes de PHP, `FILTER_FLAG_GLOBAL_RANGE` est utile si vous ciblez uniquement des adresses globalement routables. Je le vois comme un filtre de confort, pas comme une excuse pour négliger la logique métier : une IP valide n’est pas forcément une IP pertinente pour votre cas d’usage. Et une fois validée, la question suivante devient vite celle du stockage et des conversions.
Convertir et conserver les adresses sans casser l’IPv6
Si votre code historique est encore centré sur `ip2long()` et `long2ip()`, je recommande de le traiter comme un héritage IPv4. Ces fonctions fonctionnent, mais elles sont limitées à IPv4 et elles se heurtent vite aux architectures 32 bits, où certaines valeurs peuvent devenir négatives. Pour les projets modernes, je préfère `inet_pton()` et `inet_ntop()`, parce qu’ils gèrent IPv4 et IPv6 avec une représentation binaire propre.
| Fonction | Usage principal | Avantage | Limite |
|---|---|---|---|
| `ip2long()` | IPv4 vers entier | Simple sur du code ancien | IPv4 uniquement, pièges de signe sur 32 bits |
| `long2ip()` | Entier vers IPv4 | Retour lisible | Ne couvre pas IPv6 |
| `inet_pton()` | Texte vers binaire | IPv4 et IPv6, stockage compact | Renvoie `false` si la syntaxe est invalide |
| `inet_ntop()` | Binaire vers texte | Reconstruction fiable et portable | Suppose que l’entrée est bien binaire |
bindValue(':ip', $packed, PDO::PARAM_LOB);
// Exemple de retour lisible
$displayIp = inet_ntop($packed);
Si vous stockez en base, un champ binaire de 16 octets couvre IPv6, et donc aussi IPv4 si vous gardez la même stratégie de représentation. Quand je veux une lecture plus simple dans les requêtes métier, j’ajoute parfois un indicateur de version ou je normalise la couche SQL de la même façon que la couche PHP. L’idée n’est pas d’optimiser pour l’esthétique, mais d’éviter les comparaisons fragiles et les conversions répétées. À ce stade, il reste un volet souvent sous-estimé : la sécurité et la conformité autour de ces données.
Sécurité, logs et conformité quand l’adresse devient une donnée sensible
Une IP n’est pas une identité, et je me méfie de tout ce qui la traite comme telle. Derrière un VPN, un mobile carrier-grade NAT, un proxy d’entreprise ou un réseau partagé, plusieurs personnes peuvent sortir avec la même adresse publique. En sens inverse, une même personne peut changer d’IP plusieurs fois dans la journée. C’est exactement pour cela qu’une IP doit servir de signal, pas de preuve unique.
En France, la CNIL rappelle qu’une adresse IP peut être une donnée personnelle. Dans la pratique, cela m’amène à minimiser ce que je conserve : je garde le brut pour ce qui est nécessaire au diagnostic ou à la sécurité, puis je réduis, j’agrège ou je pseudonymise dès que je peux. Sur des logs d’accès, beaucoup d’équipes travaillent avec une conservation brute de quelques semaines à quelques mois, avant de basculer vers des statistiques plus légères. La bonne durée dépend de la finalité, pas d’une habitude de développeur.
Quand j’ai besoin d’une résolution inverse, j’utilise `gethostbyaddr()` avec prudence. C’est utile pour l’administration ou certains rapports, mais pas pour un chemin critique de requête. Si le DNS inverse échoue, la fonction peut simplement retourner l’adresse d’origine, ce qui suffit à montrer que ce n’est pas un mécanisme d’identification fiable. Je l’utilise donc comme un enrichissement, jamais comme un fondement de décision.
Le bon réflexe, au fond, consiste à associer l’IP à d’autres signaux : compte, session, jeton, fenêtre temporelle, ou règle de limitation de débit. C’est cette combinaison qui réduit les faux positifs et les abus sans transformer l’application en système intrusif. Et si je dois choisir entre plus de données et moins de risque, je prends presque toujours moins de données, parce que c’est plus simple à défendre et plus simple à maintenir.
Le workflow que je garde pour un code robuste en production
Quand je dois intégrer la gestion des IP dans une application PHP, je garde une séquence fixe. Elle n’est pas spectaculaire, mais elle évite la plupart des erreurs que je vois en revue de code.
- Je lis d’abord `REMOTE_ADDR` comme point d’entrée, puis je vérifie si l’infrastructure impose un proxy de confiance.
- Je n’accepte les en-têtes de type `X-Forwarded-For` que si la requête vient d’un relais connu et maîtrisé.
- Je valide la valeur avec `FILTER_VALIDATE_IP`, puis j’ajoute les flags adaptés au besoin métier.
- Je convertis avec `inet_pton()` dès que je dois stocker, comparer ou normaliser.
- Je stocke en binaire si je veux de la robustesse et de la compatibilité IPv6, ou en texte normalisé si la lisibilité prime.
- Je conserve le minimum nécessaire dans les logs et je traite l’adresse comme une donnée personnelle potentielle.
Avec cette approche, l’IP cesse d’être un champ ambigu et devient un signal bien cadré, utile pour la sécurité, l’audit et le diagnostic sans créer de faux espoirs sur l’identification d’un visiteur. C’est la différence entre un code qui “marche chez moi” et un code qui tient encore quand l’application passe derrière un proxy, une base de données stricte ou une contrainte de conformité réelle.