Skip to content

HTTP : contourner un filtrage d'adresse IP

Certaines pages web ou fonctionnalités d’administration sont restreintes par filtrage d’adresse IP. Quand cette vérification repose sur des en-têtes HTTP contrôlables par le client plutôt que sur l’IP de connexion TCP, il est possible d’usurper l’adresse source et de contourner la restriction.

Les plages d’adresses privées (RFC 1918)

Section titled “Les plages d’adresses privées (RFC 1918)”

La RFC 1918 définit trois blocs d’adresses réservés aux réseaux privés. Ces adresses ne sont pas routables sur Internet : un paquet portant une de ces adresses comme source ou destination ne franchit pas la frontière d’un réseau d’entreprise.

BlocPlageNombre d’adresses
10.0.0.0/810.0.0.0 – 10.255.255.255~16,7 millions
172.16.0.0/12172.16.0.0 – 172.31.255.255~1 million
192.168.0.0/16192.168.0.0 – 192.168.255.255~65 536

Un serveur qui restreint l’accès aux adresses de ces plages suppose que seuls des clients internes (sur le réseau local) peuvent envoyer des requêtes avec ces adresses. C’est faux si le filtrage repose sur des en-têtes HTTP.

X-Forwarded-For est un en-tête HTTP non standard, mais largement utilisé par les proxys et load balancers pour transmettre l’adresse IP d’origine du client à travers une chaîne d’intermédiaires.

Format :

X-Forwarded-For: <client>, <proxy1>, <proxy2>

L’adresse la plus à gauche est censée être celle du client d’origine. Chaque proxy ajoute la suivante à droite.

Exemples légitimes :

X-Forwarded-For: 203.0.113.45
X-Forwarded-For: 203.0.113.45, 198.51.100.10, 198.51.100.22

Si le serveur lit X-Forwarded-For pour effectuer son contrôle d’accès sans valider que la valeur provient d’un proxy de confiance, n’importe quel client peut forger cet en-tête.

GET /admin HTTP/1.1
Host: exemple.com
X-Forwarded-For: 127.0.0.1
GET /admin HTTP/1.1
Host: exemple.com
X-Forwarded-For: 192.168.1.1

Si le code serveur ressemble à :

# Vérification naïve — contournable
client_ip = request.headers.get('X-Forwarded-For', request.remote_addr)
if client_ip not in ALLOWED_IPS:
abort(403)

Forger l’en-tête suffit à passer le contrôle.

Plusieurs en-têtes peuvent être lus par le serveur selon la configuration du proxy ou du framework :

X-Forwarded-For: 127.0.0.1
X-Forwarded-For: 192.168.0.1
X-Real-IP: 127.0.0.1
X-Real-IP: 10.0.0.1
X-Client-IP: 127.0.0.1
Forwarded: for=127.0.0.1
Forwarded: for="[::1]"
True-Client-IP: 127.0.0.1
CF-Connecting-IP: 127.0.0.1

Les adresses à tester en priorité :

127.0.0.1 # loopback — "la requête vient du serveur lui-même"
::1 # loopback IPv6
10.0.0.1 # plage privée RFC 1918
172.16.0.1 # plage privée RFC 1918
192.168.1.1 # plage privée RFC 1918
Terminal window
# Tester X-Forwarded-For avec une IP locale
curl -H "X-Forwarded-For: 127.0.0.1" https://exemple.com/admin
# Tester X-Real-IP
curl -H "X-Real-IP: 192.168.0.1" https://exemple.com/admin
# Tester l'en-tête standardisé Forwarded (RFC 7239)
curl -H "Forwarded: for=127.0.0.1" https://exemple.com/admin
# Combiner plusieurs en-têtes
curl \
-H "X-Forwarded-For: 127.0.0.1" \
-H "X-Real-IP: 127.0.0.1" \
-H "X-Client-IP: 127.0.0.1" \
https://exemple.com/admin

Un code de réponse qui change (200 au lieu de 403) confirme que le filtrage repose sur ces en-têtes.

Ne pas utiliser X-Forwarded-For pour les contrôles de sécurité

Section titled “Ne pas utiliser X-Forwarded-For pour les contrôles de sécurité”

L’IP de connexion TCP (REMOTE_ADDR en PHP, request.remote_addr en Python, req.socket.remoteAddress en Node.js) est la seule valeur non falsifiable côté client. Elle reflète l’IP qui a établi la connexion réseau.

# Correct : utiliser l'IP de connexion TCP
client_ip = request.remote_addr
if client_ip not in ALLOWED_IPS:
abort(403)

Quand l’application est derrière un reverse proxy de confiance (Nginx, AWS ALB…), deux approches valides :

Méthode par comptage : avec un seul proxy connu, lire le Nième élément en partant de la droite dans X-Forwarded-For, où N est le nombre de proxys de confiance.

# Avec exactement 1 proxy de confiance
forwarded_for = request.headers.get('X-Forwarded-For', '')
ips = [ip.strip() for ip in forwarded_for.split(',')]
# L'IP cliente est la dernière ajoutée par le proxy de confiance
client_ip = ips[-(1 + nombre_proxys_de_confiance)]

Méthode par liste : parcourir X-Forwarded-For de droite à gauche, ignorer les IPs correspondant à des proxys connus, prendre la première inconnue.

Ne jamais lire le premier élément (le plus à gauche) : c’est celui que le client contrôle entièrement.

Configurer le filtrage côté infrastructure

Section titled “Configurer le filtrage côté infrastructure”

Le filtrage IP a plus de sens au niveau du réseau (pare-feu, règles de sécurité groupe AWS/GCP, nginx allow/deny) qu’au niveau applicatif, précisément parce qu’il ne dépend alors pas d’en-têtes HTTP :

location /admin {
allow 10.0.0.0/8;
allow 192.168.0.0/16;
deny all;
}

Un filtrage IP basé sur X-Forwarded-For ou tout autre en-tête HTTP modifiable par le client n’est pas un contrôle d’accès. La seule source d’IP fiable est l’adresse de connexion TCP. Tout le reste est une valeur déclarative que le client choisit lui-même.