La subresource integrity après polyfill.io : ce que SRI bloque, et ce qu'elle ne peut pas bloquer
La subresource integrity (SRI), c'est un attribut integrity qui contient l'empreinte cryptographique (hash) d'un fichier que tu charges depuis un autre serveur. Si le fichier qui arrive ne correspond pas à cette empreinte, le navigateur refuse de l'utiliser. Ça fait de SRI une défense solide pour des fichiers statiques et versionnés hébergés sur un CDN, et une défense nulle pour un service dont la réponse change d'une requête à l'autre, ce qui était exactement le cas de polyfill.io.
Ce guide explique comment fonctionne SRI, comment l'ajouter, pourquoi elle n'aurait pas sauvé les sites touchés par l'attaque polyfill.io en 2024, et avec quoi la combiner.
Ce qui s'est passé avec polyfill.io
Polyfill.io était un service populaire : tu ajoutais une balise script, et il envoyait à chaque navigateur les polyfills dont il avait besoin pour les fonctionnalités JavaScript qui lui manquaient. En février 2024, le domaine a changé de mains. Cloudflare a ensuite identifié l'acheteur : Funnull, une entreprise relativement inconnue.
En juin 2024, l'entreprise de sécurité Sansec a signalé que le CDN s'était mis à injecter du code malveillant dans ce qu'il servait. Plus de 100 000 sites intégraient le script depuis cdn.polyfill.io, dont JSTOR, Intuit et le Forum économique mondial parmi les utilisateurs notables. Le code injecté était écrit pour passer inaperçu. D'après Sansec, il :
- redirigeait les visiteurs mobiles vers un site de paris sportifs
- ne s'activait que sur certains appareils mobiles, à certaines heures
- restait inactif quand il détectait un utilisateur administrateur
- retardait son exécution quand il trouvait un service d'analyse d'audience
- se chargeait via un faux domaine Google Analytics,
www.googie-anaiytics.com
La réaction a été rapide une fois le rapport publié. Cloudflare et Fastly ont mis en ligne des miroirs sûrs, Cloudflare a commencé à réécrire les liens polyfill.io sur les sites qui passent par son proxy (automatiquement sur l'offre gratuite), Google s'est mis à bloquer les publicités des sites e-commerce qui utilisaient le script, et Namecheap a suspendu le domaine. Une mise à jour ultérieure de Sansec a relié le même acteur à d'autres domaines de CDN, dont bootcdn.net, bootcss.com, staticfile.org et staticfile.net, utilisés pour diffuser des logiciels malveillants depuis au moins juin 2023.
Aucun des sites touchés n'avait modifié une seule ligne de code. Une balise script ajoutée des années plus tôt suffisait.
Comment fonctionne la subresource integrity
Tu donnes au navigateur l'empreinte attendue du fichier. Il télécharge le fichier, calcule lui-même l'empreinte, et compare :
<script
src="https://cdn.example.com/lib@4.2.1/lib.min.js"
integrity="sha384-REPLACE_WITH_REAL_HASH"
crossorigin="anonymous"></script>
La référence de MDN détaille les règles qui comptent en pratique :
- Où ça marche : les éléments
<script>, et les éléments<link>avecrel="stylesheet",rel="preload"ourel="modulepreload". - Algorithmes : les préfixes autorisés sont
sha256,sha384etsha512. - En cas de non-correspondance : le navigateur refuse de charger la ressource et renvoie une erreur réseau. Le script ne s'exécute tout simplement pas.
- Plusieurs empreintes : tu peux lister plusieurs valeurs avec le même algorithme, et le fichier se charge si l'une d'elles correspond.
- CORS : les requêtes cross-origin avec vérification d'intégrité doivent passer par CORS, d'où l'attribut
crossoriginobligatoire. Le CDN doit envoyer les en-têtes CORS correspondants, ce que font les grands CDN publics.
Générer une empreinte SRI
Télécharge le fichier exact que tu vas référencer, vérifie que c'est bien la version attendue, et calcule son empreinte. MDN donne cette commande :
cat lib.min.js | openssl dgst -sha384 -binary | openssl base64 -A
Ajoute le préfixe sha384- devant le résultat et place-le dans l'attribut integrity. Sur une machine sans OpenSSL, shasum fait le même travail :
shasum -b -a 384 lib.min.js | awk '{ print $1 }' | xxd -r -p | base64
L'URL doit pointer vers une version fixe. Un chemin comme lib@latest ou une URL sans version changera sans prévenir, l'empreinte cessera de correspondre, et le script disparaîtra de ton site sans un bruit.
Pourquoi SRI n'aurait pas pu protéger les utilisateurs de polyfill.io
Le README du projet décrit sa fonctionnalité centrale : le service lit l'en-tête User-Agent de chaque requête et renvoie les polyfills adaptés à ce navigateur. Chrome, Safari et un vieux navigateur Android recevaient chacun un fichier différent.
Une empreinte décrit un fichier exact. Une réponse qui varie selon le navigateur n'a pas d'empreinte correcte unique : les sites qui utilisaient polyfill.io n'avaient donc, dès le départ, aucun moyen pratique de l'épingler avec SRI. Et le comportement des attaquants renforce encore ce constat : du code qui change selon l'appareil et l'heure de la journée, c'est l'exact opposé de ce que SRI sait décrire.
SRI te protège contre un fichier remplacé derrière une URL qui ne devrait jamais changer. Elle n'aide pas avec les services conçus pour changer, comme les polyfills dynamiques, les gestionnaires de balises (tag managers), les chargeurs d'outils d'analyse ou les extraits de tests A/B.
Ce qui protège vraiment un script tiers
1. Héberge toi-même ou intègre au bundle ce que tu peux. Si une bibliothèque n'a pas besoin d'être servie par quelqu'un d'autre, installe-la comme dépendance et sers-la depuis ta propre origine. La vente d'un domaine à l'autre bout du monde ne peut pas toucher un fichier que tu sers toi-même. Pour les polyfills, Sansec a relayé le conseil de l'auteur d'origine : les navigateurs modernes n'ont plus du tout besoin de ce service ; si tu prends encore en charge des navigateurs plus anciens, laisse ton outil de build inclure les polyfills en fonction de tes navigateurs cibles.
2. SRI sur ce qui reste externe et statique. Une bibliothèque de graphiques épinglée à une version exacte sur un CDN public, ou un widget publié sous une URL versionnée, devrait porter une empreinte.
3. Une liste d'autorisation Content-Security-Policy. Une politique comme script-src 'self' https://js.stripe.com limite les origines qui peuvent exécuter des scripts sur tes pages. Elle n'aurait pas bloqué polyfill.io lui-même si tu avais autorisé cet hôte, mais le code injecté allait chercher un second script sur un domaine sosie, et une liste d'autorisation sans ce domaine bloque cette seconde étape.
4. Integrity-Policy, là où c'est pris en charge. Un en-tête de réponse plus récent, Integrity-Policy: blocked-destinations=(script), demande au navigateur de bloquer tout script chargé sans métadonnées d'intégrité. MDN le signale comme d'une disponibilité limitée : considère-le pour l'instant comme une couche supplémentaire, et rappelle-toi qu'il s'applique aussi à tes propres bundles.
Les scripts que tu ne peux pas héberger toi-même
Certains fournisseurs exigent un chargement depuis leur domaine. La documentation de Stripe indique que Stripe.js doit toujours être chargé directement depuis https://js.stripe.com, et non intégré à un bundle ou hébergé par tes soins, et recommande de l'inclure sur chaque page pour que sa détection de fraude puisse voir comment les clients naviguent. L'URL de Stripe est versionnée, mais c'est le fournisseur qui contrôle le fichier.
Pour ce genre de scripts, la décision relève de la confiance plutôt que des empreintes. Garde la liste courte, autorise ces hôtes exacts dans ta CSP, et sache quel fournisseur peut modifier du code sur ta page de paiement.
Audite les scripts externes de ton propre site
Ouvre ton site, puis la console du navigateur, et lance :
[...document.scripts]
.filter(s => s.src && new URL(s.src).origin !== location.origin)
.map(s => ({ src: s.src, integrity: s.integrity || '(none)' }))
Chaque ligne est du code que tu ne contrôles pas. Pour chaque entrée, réponds à trois questions : doit-il vraiment être externe, la version est-elle épinglée dans l'URL, et porte-t-il une valeur integrity ? Recommence sur quelques pages, parce que les pages de paiement et les pages marketing chargent souvent des scripts différents. Les scripts injectés plus tard par un tag manager n'apparaîtront pas dans cette liste, ce qui est une raison de plus pour examiner ce que charge le tag manager.
FAQ
Qu'est-ce que la subresource integrity, en termes simples ?
C'est une empreinte que tu places dans une balise <script> ou <link> pour que le navigateur puisse vérifier qu'un fichier venant d'un CDN est exactement celui que tu as testé. Si un seul octet diffère, le navigateur refuse de le charger.
Puis-je utiliser SRI avec Google Analytics ou un tag manager ?
Pas d'une façon utile. Ces scripts sont mis à jour par le fournisseur sans changement de version dans l'URL : une empreinte casserait dès la sortie d'une nouvelle version. Restreins-les plutôt avec une liste d'autorisation CSP.
Pourquoi faut-il crossorigin="anonymous" avec SRI ?
Les navigateurs ne font les vérifications d'intégrité que sur les fichiers cross-origin récupérés via CORS. Sans l'attribut crossorigin, la requête n'est pas faite en mode CORS et la vérification ne peut pas réussir : le fichier ne se chargera pas.
polyfill.io représente-t-il encore un risque ?
Sansec a rapporté que Namecheap avait suspendu le domaine en juin 2024, et Cloudflare a activé la réécriture automatique des liens pour les sites de son offre gratuite. Ni l'un ni l'autre n'est une raison de garder la balise : toute page qui référence encore polyfill.io devrait la retirer ou passer à une alternative intégrée au bundle.