Retour au blogSécurité

Injection CSS : comment exfiltrer du texte sans une ligne de JavaScript

CZSyn
5 juillet 2026
8 min

Bench Press (Hack.lu 2024) : une simple injection CSS suffit à exfiltrer le texte d'une page web sans JavaScript. Ce qu'il faut verrouiller.

Illustration d'une exfiltration de données depuis une page web via une injection CSS, sans JavaScript
Ce qu'il faut retenir
  1. Le chercheur pspaul a publié la solution complète de Bench Press, un challenge créé pour le CTF Hack.lu 2024, qui démontre qu'on peut exfiltrer l'intégralité d'un texte HTML (par exemple un token d'authentification) en utilisant uniquement du CSS, sans aucune ligne de JavaScript.
  2. La technique combine unicode-range et descent-override pour donner une hauteur unique à chaque caractère, puis des animations liées à une timeline de scroll pour mesurer la hauteur d'un élément en CSS pur et la convertir en variable exploitable via calc().
  3. La faille du challenge venait d'un filtre par liste noire (bloquant seulement la séquence </) sur un paramètre theme interpolé dans une balise <style>, combiné à une CSP qui autorisait style-src 'unsafe-inline' et img-src distants en pensant que l'absence de script-src suffisait à protéger les données.

Résumé généré par IA

En 2024, lors du CTF Hack.lu, le chercheur en sécurité pspaul a conçu un challenge nommé Bench Press. Il vient d'en publier la solution complète, et le résultat mérite l'attention de tout développeur qui génère des styles CSS dynamiques côté serveur : il est possible d'exfiltrer l'intégralité d'un texte HTML, y compris un token d'authentification, en n'utilisant que du CSS. Aucune ligne de JavaScript n'est nécessaire.

Pendant longtemps, l'injection CSS a été perçue comme un risque mineur. Un attaquant qui parvient à injecter des styles ne peut pas exécuter de code, contrairement à une injection JavaScript classique (XSS). Beaucoup d'équipes autorisent donc style-src 'unsafe-inline' dans leur Content Security Policy (CSP) tout en verrouillant script-src, persuadées que le pire qui puisse arriver est un défacement visuel. Le travail de pspaul démontre que cette hypothèse ne tient plus : le CSS moderne, avec ses sélecteurs, ses animations liées au scroll et ses polices personnalisées, suffit à construire un canal d'exfiltration de données complet.

Le terrain de jeu : un challenge avec une CSP presque parfaite

Le challenge Bench Press reproduit un scénario réaliste. Une application Express expose une route pour des articles, qui accepte un paramètre theme dans l'URL. Ce paramètre est injecté tel quel dans une balise <style> côté serveur. La seule protection appliquée est un filtre qui remplace le thème par une valeur neutre (*{color:red}) si la chaîne contient la séquence </. Un filtre par liste noire, donc, pas par liste blanche. Le contenu à voler est un token d'authentification, une chaîne hexadécimale de 30 caractères, posée dans une balise <script>.

Sur le papier, la CSP de l'application semble solide : style-src 'unsafe-inline' pour les styles, img-src http: https: pour des images distantes, default-src 'none' pour bloquer tout le reste, et un sandbox qui interdit explicitement l'exécution de scripts. Sans script-src, on pourrait croire l'injection CSS inoffensive. C'est précisément cette hypothèse que Bench Press vient démonter.

Les techniques connues qui ne fonctionnent pas ici

Plusieurs techniques d'exfiltration via CSS existent déjà, mais aucune ne s'applique à ce scénario précis :

  • Le @import récursif, qui permet de charger des feuilles de style distantes progressivement, ne fonctionne pas puisqu'aucune CSS externe n'est autorisée.
  • Les polices à ligatures personnalisées, une autre technique classique, sont bloquées car les polices distantes ne sont pas chargées.
  • Le sélecteur d'attribut contains, souvent utilisé pour fuiter des données planquées dans des attributs HTML, ne s'applique pas ici : le token est dans un noeud de texte, pas dans un attribut.

Reste une famille de techniques connues qui, elle, correspond aux contraintes du challenge : celles qui fuitent le jeu de caractères d'un noeud de texte en mesurant une différence de taille. Le principe : pour chaque caractère possible, une police définie via unicode-range ne s'applique que si ce caractère est présent dans le texte. Si la police correspond, la hauteur du texte change, ce qui déclenche le chargement d'une image de fond utilisée comme témoin d'exfiltration : une requête part vers un domaine contrôlé par l'attaquant, encodant le caractère trouvé.

Le problème : cette méthode ne détecte que la présence d'un caractère, pas ses répétitions. Sur un texte comme LOL, une fois que L et O ont chacun déclenché une requête, aucune requête supplémentaire n'est envoyée pour le second L. Résultat : on sait que le texte contient un L et un O, mais pas combien de fois ni dans quel ordre. Pour un token hexadécimal de 30 caractères, truffé de répétitions, cette limite est rédhibitoire.

La méthode Bench Press : mesurer la hauteur du texte en CSS pur

La contribution de pspaul consiste à combiner cette approche avec une technique plus récente : mesurer la hauteur d'un élément HTML en pixels, sous forme de nombre exploitable dans une variable CSS, grâce aux animations liées à une timeline de défilement. En résumé, un pseudo-élément invisible sert de repère de scroll, une animation CSS progresse en fonction de la position de ce repère, et un calcul en calc() convertit cette progression en une valeur numérique qui représente la hauteur réelle de l'élément.

Une fois cette brique posée, le plan tient en quatre étapes :

  1. Forcer un caractère par ligne, avec une police à chasse fixe (monospace), une largeur figée (11px dans le challenge) et le retour à la ligne activé.
  2. Donner à chaque caractère possible une hauteur unique, via le descripteur descent-override d'un @font-face ciblé par unicode-range (par exemple, forcer la lettre A à 200% de sa hauteur normale).
  3. Réduire progressivement, caractère par caractère, la portion de texte visible via ::first-line, et mesurer la hauteur totale à chaque étape.
  4. Calculer la différence de hauteur entre deux étapes consécutives : elle correspond exactement au caractère qui vient d'être masqué, ce qui permet d'envoyer une requête réseau vers un serveur contrôlé par l'attaquant pour exfiltrer ce caractère précis.

Contrairement à la technique de charset classique, cette méthode ne se contente pas de savoir quels caractères sont présents : elle les extrait un par un, dans l'ordre, y compris les répétitions. C'est exactement ce qu'il faut pour reconstituer un token entier caractère par caractère.

@font-face {
  font-family: has_A;
  src: local('DejaVu Sans Mono');
  unicode-range: U+41;
  descent-override: 200%;
}

Les limites de la technique

pspaul liste lui-même les limites de sa méthode. Les imprécisions liées aux calculs en virgule flottante peuvent fausser une mesure de hauteur. La fonctionnalité d'animation liée au scroll n'est aujourd'hui disponible que sur Chrome. La technique dépend de polices locales déjà installées sur la machine de la victime, via local() dans @font-face. Et l'ensemble reste lent : chaque caractère nécessite plusieurs itérations de mesure. Autant de raisons pour lesquelles ce n'est pas, pour l'instant, une technique d'exploitation de masse. Mais elle reste redoutable dans un scénario ciblé, sur un navigateur précis, avec suffisamment de temps pour laisser tourner le CSS injecté chez la victime.

Ce que les devs front doivent verrouiller

Le point de départ du problème n'est pas le CSS en lui-même : c'est l'interpolation d'une entrée utilisateur non fiable dans un contexte CSS, combinée à une confiance mal placée dans la CSP. Quelques réflexes concrets pour un projet Node/Express, Laravel ou toute autre stack qui génère du CSS dynamique côté serveur :

  • Ne jamais interpoler du texte libre dans une balise <style> ou un attribut style. Si un thème est personnalisable, validez-le contre une liste blanche de valeurs connues, mappées côté serveur vers du CSS fixe, jamais contre une liste noire de motifs interdits comme le </ du challenge.
  • Traiter tout point d'injection CSS avec la même rigueur qu'une injection HTML ou SQL : l'absence de JavaScript possible ne veut pas dire absence de risque.
  • Verrouiller style-src dans la CSP avec des nonces plutôt qu'avec 'unsafe-inline', exactement comme vous le faites déjà pour script-src.
  • Limiter img-src et éviter le chargement de polices ou de ressources distantes déclenché par des sélecteurs CSS générés depuis une donnée utilisateur.
  • Garder un oeil sur les nouvelles fonctionnalités CSS (animations liées au scroll, @property, container queries) : chaque ajout de puissance expressive au CSS élargit aussi sa surface d'attaque.

Notre lecture chez CZSyn

Dans nos audits, on croise régulièrement ce réflexe : la CSP est verrouillée sur script-src, considérée comme la seule porte d'entrée dangereuse, et style-src est laissé grand ouvert parce que « ce n'est que du CSS ». Le travail de pspaul rappelle qu'un standard web suffisamment riche redevient, à terme, un vecteur d'exfiltration à part entière. Pour une PME ou une agence qui gère des thèmes personnalisables, un CMS ou une interface multi-tenant, la vraie leçon n'est pas de bannir les styles dynamiques, mais de les traiter comme n'importe quelle sortie utilisateur : avec un contexte d'échappement précis et une liste blanche stricte, jamais un filtre de motifs interdits bricolé à la va-vite. C'est exactement ce filtre naïf (bloquer </) qui a ouvert la porte dans ce challenge, et c'est un pattern qu'on croise encore trop souvent en production.

Votre CSP protège-t-elle vraiment vos données ?

Nous auditons vos en-têtes de sécurité, vos points d'injection CSS et la robustesse de vos formulaires de personnalisation. Audit gratuit sous 24h, développement sur-mesure et dépannage en cas d'incident.

29 AVIS 5/5 · +200 PROJETS LIVRÉS · RÉPONSE EXPRESS

Sources primaires

Un projet en tête ?

Discutons de votre projet et voyons comment nous pouvons vous aider.

Nous contacter