Guides · 13 minutes de lecture
Attaques par en-tête Host : comprendre le risque et corriger
Apache et nginx reconstruisent leurs URL à partir du nom annoncé par le visiteur : c’est leur réglage par défaut, documenté, et presque personne ne le change. Voici ce que ça permet, et le test qui tient en une commande.
Un en-tête que personne ne traite comme une saisie utilisateur
Toute requête HTTP/1.1 porte un en-tête Host. Il indique au serveur quel
site, parmi les dizaines qu’il héberge peut-être, la requête vise. Sans lui, une machine
dont l’adresse IP sert dix domaines n’aurait aucun moyen de savoir lequel servir.
Le point que presque tout le monde manque : cet en-tête est envoyé par le
client. Au même titre qu’un champ de formulaire, un paramètre d’URL ou un
cookie. Le navigateur le remplit honnêtement avec le domaine tapé dans la barre
d’adresse, mais rien n’y oblige : curl, un script de trois lignes ou
un proxy d’interception y mettent ce qu’ils veulent.
Une faille par en-tête Host, c’est donc toujours la même erreur, répétée sous des formes différentes : du code serveur qui fabrique une URL, un lien ou un e-mail à partir d’une valeur que l’attaquant contrôle, en croyant qu’elle vient du serveur.
Ce que dit la norme, et ce qu’elle ne dit pas
La spécification HTTP/1.1 est nette sur la forme. Un serveur doit répondre
400 Bad Request à toute requête HTTP/1.1 qui n’a pas d’en-tête
Host, qui en a plusieurs, ou dont la valeur est syntaxiquement invalide
(RFC 9112, section 3.2). Une requête peut aussi porter sa cible en « forme
absolue » — GET https://exemple.fr/page HTTP/1.1 — et dans ce cas les
mandataires et serveurs d’origine doivent ignorer le Host reçu et
lui substituer l’hôte de la ligne de requête.
Ce que la norme ne dit nulle part, en revanche, c’est que la valeur du Host
serait fiable. La conformité syntaxique n’est pas une validation.
Host: evil.example est un en-tête parfaitement légal. Il n’appartient qu’au
serveur — ou au code applicatif — de décider s’il accepte de répondre pour ce nom-là.
Le test tient en une commande
On envoie une requête à l’adresse IP réelle du site, en annonçant un tout autre nom d’hôte, et on regarde ce qui revient :
curl -sI https://exemple.fr/ --resolve verif.example:443:203.0.113.10 \
--connect-to exemple.fr:443:verif.example:443
Plus simplement, sur un site en clair ou depuis la machine elle-même :
curl -sI http://203.0.113.10/ -H "Host: verif.example"
Trois réponses possibles, et elles ne se valent pas.
- Un
Location:qui contientverif.example— le serveur reconstruit ses URL à partir de ce que vous lui avez dit. C’est le cas le plus courant, et celui qui alimente la plupart des exploitations. - Une page normale, avec
verif.exampledans le HTML (balise<link rel="canonical">,og:url, liens absolus, formulaire de connexion) — même problème, avec en prime un risque d’empoisonnement de cache. - Un 400, un 404, ou une redirection vers le domaine canonique — le serveur refuse de répondre pour un nom qu’il ne connaît pas, ou impose le sien. C’est le comportement correct.
Pourquoi un serveur ordinaire échoue à ce test
Ce n’est pas une négligence d’administrateur : c’est le réglage par
défaut. Sur Apache, la directive UseCanonicalName vaut
Off par défaut, et la documentation le dit sans détour : dans ce mode,
le serveur construit ses URL auto-référentielles « en utilisant les valeurs de
l’en-tête Host: envoyé par le client ». Il ne se rabat sur le
ServerName configuré que si le client n’a pas envoyé de Host
du tout.
Nginx fait la même chose. server_name_in_redirect vaut off par
défaut : « lorsque l’usage du nom de serveur principal est désactivé, c’est le
nom de l’en-tête Host de la requête qui est utilisé ». Et la variable
$host, celle qu’on recopie machinalement dans les configurations trouvées
en ligne, prend d’abord le nom de la ligne de requête, puis celui de l’en-tête
Host, et seulement en dernier recours le server_name.
Ajoutez à cela le comportement du serveur virtuel par défaut : une
requête dont le Host ne correspond à aucun bloc configuré n’est pas rejetée,
elle est servie par le premier bloc déclaré sur ce couple adresse : port. Sur
un mutualisé, ce premier bloc peut être le site d’un autre client. Sur un serveur que
vous administrez, c’est souvent votre propre site — qui répond donc, poliment, à
n’importe quel nom.
Empoisonnement de la réinitialisation de mot de passe
C’est l’exploitation la plus citée, et la plus mal racontée. Le principe théorique est
simple : si l’application fabrique le lien de réinitialisation avec
$_SERVER['HTTP_HOST'], l’attaquant demande une réinitialisation pour le
compte de sa victime en envoyant Host: evil.example. La victime reçoit un
vrai e-mail, du vrai site, contenant un lien vers le serveur de l’attaquant — et son
jeton de réinitialisation avec.
Le cas réel le plus documenté est plus retors que ça, et c’est important de le dire
correctement. CVE-2017-8295, publiée en mai 2017, concerne WordPress
jusqu’à la version 4.7.4. WordPress construit ses liens à partir des options
siteurl et home stockées en base : le lien de
réinitialisation, lui, n’est pas empoisonnable. Ce qui l’était, c’est
l’adresse d’expéditeur du courriel, dérivée de SERVER_NAME
dans wp-includes/pluggable.php — donc du Host sous une
configuration Apache par défaut. L’attaquant obtenait un message dont le
From et le Return-Path pointaient chez lui, et récupérait le
jeton si le courriel rebondissait, si un répondeur automatique le renvoyait, ou si la
victime répondait en citant le message.
La leçon n’est pas « WordPress est troué » — la configuration recommandée
(WP_HOME et WP_SITEURL figées dans wp-config.php,
et un expéditeur explicite) neutralise le problème. La leçon est que
l’exploitation passe rarement par le chemin qu’on avait imaginé :
ici, pas par le lien, mais par une en-tête de courriel que personne n’avait regardée.
Empoisonnement de cache
Le scénario qui transforme une curiosité en incident. Si une couche de cache — CDN,
Varnish, cache de plugin — mémorise la réponse en n’indexant que la méthode et le chemin,
et que la réponse contient le Host reflété, alors l’attaquant
empoisonne une page pour tous les visiteurs suivants.
La charge n’a pas besoin d’être spectaculaire. Une page d’accueil dont la balise
<script src="//evil.example/app.js"> a été reconstruite avec l’hôte
de l’attaquant suffit : chaque visiteur légitime exécute du code tiers pendant toute
la durée de vie de l’entrée en cache. C’est le point où le sujet cesse d’être théorique.
Deux garde-fous : ne jamais laisser le Host se retrouver dans une
réponse mise en cache, et — si la reflétion est inévitable — inclure le Host
dans la clé de cache. Le second est un pansement ; le premier est la correction.
Contournement d’accès et SSRF par routage
Deux usages moins fréquents mais plus graves, qui supposent une architecture à plusieurs étages.
Contournement de contrôle d’accès. Beaucoup d’organisations exposent une
interface d’administration sur un nom interne (admin.interne) servi par la
même infrastructure, en comptant sur le fait que le nom n’est pas résolvable depuis
l’extérieur. Mais le routage se fait sur le Host, pas sur le DNS :
atteindre l’adresse IP publique en annonçant Host: admin.interne peut
suffire à obtenir l’interface.
SSRF par routage. Sur un répartiteur de charge ou un mandataire inverse
qui décide de la machine de destination d’après le Host, une valeur choisie
par l’attaquant peut faire router sa requête vers une machine interne — celle des
métadonnées d’instance de l’hébergeur, par exemple. La requête part alors de l’intérieur
du réseau, avec tout ce que cela autorise. C’est le même mécanisme de fond que le
SSRF classique, mais il emprunte l’infrastructure au lieu du code applicatif.
Et les en-têtes qui remplacent le Host
Fermer le Host ne suffit pas si l’on a laissé ouverte la porte de service.
Les mandataires inverses et les CDN ajoutent des en-têtes de suivi que beaucoup de
cadriciels honorent par défaut :
X-Forwarded-Host— remplace purement et simplement leHostdans nombre de bibliothèques ;X-Forwarded-ProtoetX-Forwarded-Port— décident du schéma et du port des URL construites ;X-Original-URL,X-Rewrite-URL,Forwarded— selon la pile.
La règle est simple : ces en-têtes ne doivent être crus que s’ils viennent d’un mandataire que vous administrez. Concrètement, cela veut dire déclarer la liste des mandataires de confiance dans le cadriciel, et faire supprimer ces en-têtes par la première machine qui reçoit le trafic. S’il n’y a pas de mandataire du tout, la bonne valeur est : aucun en-tête de ce type n’est cru.
Ce qui n’est pas une faille
Les scanners automatiques signalent beaucoup de faux positifs sur ce sujet. Trois comportements sont sains et ne demandent aucune correction :
- Une redirection vers votre domaine canonique. Si vous envoyez
Host: verif.exampleet que le serveur répondLocation: https://votre-domaine.fr/, il a fait exactement ce qu’il fallait : il a imposé son nom. - Un 404 ou un 400. Le serveur refuse de répondre pour un nom qu’il ne connaît pas. C’est le comportement le plus propre.
- Un domaine reflété qui vous appartient déjà — un site multi-domaines légitime, avec une liste blanche derrière. Le test ne le distingue pas d’un défaut, seule la configuration le dit.
- Une page d’erreur qui cite le nom demandé. C’est le faux positif le
plus fréquent, et nous l’avons rencontré en construisant notre propre détection :
l’erreur 1001 de Cloudflare répond
409avec une page intitulée « DNS resolution error | nom demandé ». Le nom est bien là, dans le titre et deux fois dans le texte — mais le serveur ne l’a pas repris à son compte : il explique qu’il ne le connaît pas. Citer un nom dans un message d’erreur n’est pas le refléter. Un test correct ne compte donc une reflétion que sur une réponse 2xx, ou sur une redirection 3xx qui pointe vers ce nom.
À l’inverse, un Location qui reprend un nom que vous n’avez jamais déclaré
est un fait, pas une hypothèse. Il ne prouve pas qu’une exploitation est possible ;
il prouve que la valeur circule.
Corriger : Apache
Deux mesures, dans cet ordre. D’abord, un serveur virtuel par défaut qui attrape tout ce qui ne correspond à rien et refuse de répondre :
<VirtualHost *:443>
ServerName catchall.invalid
<Location />
Require all denied
</Location>
</VirtualHost>
Ce bloc doit être le premier déclaré pour ce couple adresse : port : Apache retient le premier serveur virtuel comme défaut. Ensuite, sur vos hôtes réels, forcez le nom canonique :
ServerName votre-domaine.fr
UseCanonicalName On
Sur un hébergement mutualisé, vous n’avez pas la main sur les serveurs virtuels, mais
UseCanonicalName On passe généralement en .htaccess. À défaut,
une redirection explicite fait le travail — en écrivant le domaine en dur, jamais avec
%{HTTP_HOST} :
RewriteEngine On
RewriteCond %{HTTP_HOST} !^votre-domaine\.fr$ [NC]
RewriteRule ^(.*)$ https://votre-domaine.fr/$1 [R=301,L]
Corriger : nginx
Même logique. Un bloc attrape-tout qui coupe la connexion sans répondre, puis le nom canonique imposé dans les redirections :
server {
listen 443 ssl default_server;
server_name _;
return 444;
}
server {
listen 443 ssl;
server_name votre-domaine.fr;
server_name_in_redirect on;
# ...
}
Et dans les configurations que vous recopiez : remplacez $host et
$http_host par le nom écrit en clair partout où la valeur finit dans une
réponse — proxy_set_header Host, return 301, en-têtes de cache.
Corriger : côté application
La configuration serveur ne protège pas le code qui lit lui-même l’en-tête. En PHP nu,
la règle est de ne jamais utiliser $_SERVER['HTTP_HOST'] pour construire
une URL qui sera envoyée à quelqu’un. Une constante suffit :
define('SITE_URL', 'https://votre-domaine.fr');
// et non : 'https://' . $_SERVER['HTTP_HOST']
Si vous devez absolument accepter plusieurs domaines, comparez à une liste close, et
refusez ce qui n’y figure pas — jamais l’inverse, jamais une expression régulière
permissive. Les cadriciels courants ont tous ce mécanisme :
ALLOWED_HOSTS en Django, framework.trusted_hosts en Symfony,
le greffon TrustHosts en Laravel, WP_HOME et
WP_SITEURL pour WordPress. Aucun n’est activé de façon utile par défaut.
Enfin, le plus simple et le plus robuste : préférez les URL relatives.
Un href="/connexion" ne peut pas être empoisonné. Le problème ne se pose que
pour les URL absolues — courriels, flux, balises canoniques, redirections — et il faut,
là, une source de vérité qui ne vienne pas de la requête.
Vérifier après correction
Rejouez la commande du début. Vous devez obtenir un 400, un 404, ou une redirection vers
votre propre domaine — et plus aucune trace du nom fantaisiste, ni dans les en-têtes, ni
dans le corps. Vérifiez les deux : une reflétion peut disparaître du
Location et rester dans une balise canonical.
Testez aussi X-Forwarded-Host séparément, avec le vrai Host :
curl -sI https://votre-domaine.fr/ -H "X-Forwarded-Host: verif.example"
C’est ce que fait notre analyseur, automatiquement, sur
toute adresse que vous lui donnez : il envoie une requête portant un nom d’hôte de
contrôle, une autre portant X-Forwarded-Host, et regarde si l’un ou l’autre
ressort dans la redirection ou dans la page. Le résultat figure dans le groupe
« Chiffrement et sécurité » du rapport.
Deux sujets voisins méritent le même quart d’heure : les en-têtes de sécurité HTTP, dont plusieurs limitent précisément ce qu’une page empoisonnée peut faire, et le contenu mixte, qui relève de la même famille de défauts : une URL construite avec la mauvaise source de vérité.
Passer à l’action
À lire ensuite
Les en-têtes de sécurité HTTP, expliqués un par un →
Six en-têtes, ce qu’ils protègent réellement, et la configuration à copier pour Apache, nginx ou WordPress. Sans culte du cargo.
Contenu mixte : votre site HTTPS s’affiche « non sécurisé » →
Une ressource en http:// suffit à casser une page en HTTPS. Ce que le navigateur bloque, ce qu’il surclasse en silence, et pourquoi le second cas est le plus dangereux.
Ce guide est informatif et ne constitue pas un conseil juridique. La réglementation
évolue et son application dépend de votre situation.
Publié le 31/08/2026.
Sources : RFC 9112, section 3.2 (obligation de répondre 400 en l’absence de Host, en cas de doublon ou de valeur invalide, et traitement de la forme absolue) ; documentation Apache, UseCanonicalName et documentation nginx, server_name_in_redirect et $host (valeurs par défaut citées) ; OWASP WSTG-INPV-17, Testing for Host Header Injection (typologie des exploitations) ; CVE-2017-8295 (WordPress jusqu’à 4.7.4). Le faux positif de l’erreur 1001 de Cloudflare a été mesuré par nos soins le 31/08/2026. Liens relevés le 31/08/2026.