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 contient verif.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.example dans 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 le Host dans nombre de bibliothèques ;
  • X-Forwarded-Proto et X-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.example et que le serveur répond Location: 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 409 avec 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é.

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.