Aller au contenu principal

Vulnérabilités des headers HTTP

Schéma

Ce TP utilise 2 machines : un serveur sous Linux Debian, qui héberge le site avec Apache, et un client sous Windows, qui s'y connecte avec son navigateur.

  • Comprendre comment l'absence de certains headers HTTP laisse une application exposée à des attaques (Clickjacking, XSS, interception) alors même que le serveur et le code fonctionnent normalement : un cas particulier de Security Misconfiguration, n°2 du Top 10 de l'OWASP (2025).
  • Apprendre à ajouter ces headers avec Apache, grâce au module mod_headers.
attention

Un header de sécurité ne corrige jamais une faille présente dans le code : il ajoute une protection supplémentaire, au niveau du navigateur, qui limite les dégâts si une faille existe quand même. C'est une protection en profondeur, pas un substitut à du code bien écrit.


1. Création du site vulnérable​

Sur le serveur Debian, créez un dossier /var/www/html/club-randonnee/ contenant la page de recherche d'adhérents du club de randonnée.

index.php :

<?php
$nom = $_GET['nom'] ?? '';
?>
<!DOCTYPE html>
<html lang="fr">
<head><meta charset="UTF-8"><title>Club de randonnée - Recherche d'adhérents</title></head>
<body>
<h1>Club de randonnée</h1>
<form method="get" action="">
<label>Rechercher un adhérent par nom : <input type="text" name="nom"></label>
<button type="submit">Rechercher</button>
</form>
<?php if ($nom !== ''): ?>
<p>Résultat de la recherche : <?= $nom ?></p>
<?php endif; ?>
</body>
</html>
info

<?= $nom ?> affiche directement la valeur du paramètre nom, sans aucun échappement : c'est volontaire, pour provoquer une faille XSS à l'étape 4, exactement comme le ferait un formulaire de recherche mal écrit sur un vrai site.


2. Démarrer Apache​

Sur le serveur Debian, installez Apache et PHP, puis placez le site dans le répertoire par défaut du serveur :

sudo apt update
sudo apt install apache2 libapache2-mod-php php -y
sudo systemctl start apache2
sudo systemctl enable apache2
info

sudo systemctl start apache2 peut aussi s'écrire sudo service apache2 start : les deux commandes démarrent Apache, service étant l'ancienne syntaxe (toujours disponible sur Debian), systemctl la syntaxe actuelle (systemd).

Relevez l'adresse IP du serveur Debian (ip addr show) : nous l'appellerons <IP_DU_SERVEUR> dans la suite du TP.

Depuis le client Windows, vérifiez qu'Apache fonctionne en ouvrant http://<IP_DU_SERVEUR>/club-randonnee/ dans votre navigateur : le formulaire de recherche doit s'afficher.


3. Vérifier l'absence de headers de sécurité​

Depuis un terminal du client Windows (PowerShell, qui inclut curl par défaut), interrogez les headers HTTP renvoyés par le serveur :

curl -I http://<IP_DU_SERVEUR>/club-randonnee/

Question : Parmi les headers affichés, lequel protège contre le Clickjacking, l'exécution de script injecté, ou l'interception en clair de la connexion ?

📌 Une réponse


4. Constater la vulnérabilité au Clickjacking​

Sur le client Windows, créez un fichier clickjacking.html :

<!DOCTYPE html>
<html>
<head><title>Page piégée</title></head>
<body>
<h2>Jeu concours : cliquez ici pour gagner !</h2>
<iframe src="http://<IP_DU_SERVEUR>/club-randonnee/" width="800" height="600"></iframe>
</body>
</html>

Ouvrez ce fichier dans le navigateur du client Windows.

Résultat attendu : le site du club de randonnée s'affiche normalement dans l'iframe.

Question : En quoi est-ce dangereux, alors que le contenu affiché est celui du vrai site ?

📌 Une réponse


5. Constater la vulnérabilité XSS liée à l'absence de CSP​

Dans le navigateur du client Windows, ouvrez :

http://<IP_DU_SERVEUR>/club-randonnee/index.php?nom=<script>alert('XSS')</script>

Résultat attendu : une alerte JavaScript s'affiche.

Question : Cette faille vient-elle d'un header manquant, ou d'autre chose ?

📌 Une réponse

attention

Jusqu'ici, aucune configuration n'a été modifiée : c'est la configuration par défaut d'Apache et de PHP qui laisse ces deux attaques possibles.


6. Ajouter les headers de sécurité avec Apache​

Sur le serveur Debian, activez le module mod_headers, indispensable pour ajouter des headers personnalisés avec Apache :

sudo a2enmod headers

Ouvrez la configuration du site :

sudo nano /etc/apache2/sites-available/000-default.conf

Ajoutez ces lignes dans le bloc <VirtualHost *:80> :

<VirtualHost *:80>
# ... lignes existantes ...

Header always set Strict-Transport-Security "max-age=31536000; includeSubDomains"
Header always set X-Frame-Options "DENY"
Header always set X-Content-Type-Options "nosniff"
Header always set Content-Security-Policy "default-src 'self'"
Header always set Referrer-Policy "no-referrer"
</VirtualHost>

Redémarrez Apache pour appliquer les changements :

sudo systemctl restart apache2
info

always indique à Apache d'ajouter le header à toutes les réponses, y compris les pages d'erreur (404, 500...), qui seraient sinon oubliées.


7. Vérifier que les headers sont bien appliqués​

Depuis le client Windows, rejouez la commande de l'étape 3 :

curl -I http://<IP_DU_SERVEUR>/club-randonnee/

Résultat attendu : les cinq headers ajoutés apparaissent désormais dans la réponse.

📌 Une réponse


8. Revérifier le Clickjacking​

Sur le client Windows, rechargez clickjacking.html dans votre navigateur.

Résultat attendu : l'iframe reste vide, et la console du navigateur affiche un refus du type « Refused to display ... in a frame because it set 'X-Frame-Options' to 'deny' ».


9. Revérifier le XSS​

Sur le client Windows, rechargez :

http://<IP_DU_SERVEUR>/club-randonnee/index.php?nom=<script>alert('XSS')</script>

Résultat attendu : l'alerte ne s'affiche plus. La console du navigateur signale que l'exécution du script a été bloquée par la politique Content-Security-Policy.

Bonne pratique - La CSP ne remplace pas l'échappement

Le paramètre nom reste affiché sans échappement dans le code source de la page : seule la CSP empêche le script de s'exécuter. Une vraie correction consiste à échapper l'affichage (htmlspecialchars($nom)), en plus de garder la CSP comme protection supplémentaire.


Notions théoriques​

Les headers HTTP sont des métadonnées envoyées avec chaque réponse d'un serveur Web. Certains d'entre eux, dits headers de sécurité, indiquent au navigateur des règles supplémentaires à appliquer pour limiter les conséquences de failles déjà présentes dans une application.

Les principaux headers de sécurité​

HeaderProtège contreEffet
Strict-Transport-Security (HSTS)Interception en clair, Man-in-the-MiddleOblige le navigateur à utiliser uniquement HTTPS pour ce site
X-Frame-OptionsClickjackingEmpêche l'affichage du site dans une iframe
Content-Security-Policy (CSP)XSSLimite les sources de script autorisées à s'exécuter
X-Content-Type-OptionsMIME sniffingEmpêche le navigateur d'interpréter un fichier autrement que son type déclaré
Referrer-PolicyFuite d'informationsLimite ce qui est transmis dans l'URL d'origine lors d'une navigation

Pourquoi ces headers sont-ils si souvent absents ?​

Aucun de ces headers n'est ajouté par défaut par Apache : il faut explicitement activer mod_headers et les déclarer, comme vu dans le TP. Une application peut donc fonctionner parfaitement, sans aucune erreur visible, tout en restant dépourvue de cette protection : c'est pour cela que l'absence de headers de sécurité est une forme de mauvaise configuration, plutôt qu'une faille de code.

remarque

Contrairement au XSS, qui porte sur une faille du code, un header de sécurité se règle au niveau du serveur Web, indépendamment du code de l'application.

Comment vérifier les headers d'un site ?​

  • En ligne de commandes, avec curl -I https://exemple.fr.
  • Dans un navigateur, via les outils de développement (F12), onglet « Réseau », en cliquant sur la requête principale.
attention

Un site protégé par des headers de sécurité n'a pas pour autant corrigé les failles de son code : comme vu à l'étape 9, la CSP empêche le script de s'exécuter, mais le paramètre nom reste affiché sans échappement. Les headers sont une protection en plus, jamais un remplacement du code correctement écrit.


Test de mémorisation/compréhension​


À quoi sert un header de sécurité HTTP ?


Quel header empêche un site de s'afficher dans une iframe ?


Dans le TP, que révèle `curl -I` avant la configuration des headers ?


Pourquoi l'iframe de clickjacking.html affiche-t-elle le site, à l'étape 4 ?


D'où vient réellement la faille XSS observée à l'étape 5 ?


Quelle commande active le module Apache nécessaire aux headers personnalisés ?


Que signifie le mot-clé `always` dans une directive `Header always set` ?


Après l'étape 6, que se passe-t-il en rechargeant clickjacking.html ?


Pourquoi l'alerte JavaScript ne s'affiche-t-elle plus à l'étape 9 ?


Pourquoi la CSP ne suffit-elle pas à considérer la faille XSS comme corrigée ?


Pourquoi l'absence de headers de sécurité est-elle une forme de mauvaise configuration ?



Bonnes pratiques​

  • Activer mod_headers et déclarer les headers de sécurité sur chaque site Apache, car aucun n'est ajouté par défaut.
  • Utiliser Header always set plutôt que Header set, pour que la protection s'applique aussi aux pages d'erreur.
  • Vérifier les headers après chaque modification (curl -I, outils de développement du navigateur), pas seulement une fois au moment de la mise en production.
  • Ne jamais considérer un header de sécurité comme un correctif du code : la CSP bloque un script injecté, elle ne remplace pas l'échappement des données affichées.
  • Combiner plusieurs headers (HSTS, X-Frame-Options, CSP, X-Content-Type-Options, Referrer-Policy) : chacun protège contre un type d'attaque différent.

Ce que fait Symfony​

Symfony, par défaut, n'ajoute pas davantage de headers de sécurité qu'Apache seul : c'est à vous de les régler, par exemple via un EventListener sur la réponse, ou en installant le bundle NelmioSecurityBundle, qui fournit des réglages prêts à l'emploi pour la plupart de ces headers.

# config/packages/nelmio_security.yaml
nelmio_security:
clickjacking:
paths:
'^/.*': DENY
content_type:
nosniff: true
csp:
enforce:
default-src: ["'self'"]
attention

Comme avec Apache, activer ces headers ne corrige jamais une faille déjà présente dans vos contrôleurs ou vos templates Twig : Twig échappe automatiquement les variables affichées ({{ nom }}), mais seulement si vous ne désactivez pas ce comportement avec le filtre |raw.