Recherche de vulnérabilitésHigh · CVSS 8.2

CVE-2026-73650 : contourner le filtre removeScripts de SVGO

Deux écarts entre la façon dont SVGO reconnaît un script et la façon dont un navigateur l'exécute. Résultat : du JavaScript qui survit à l'optimisation, et une XSS stockée pour toute application qui fait confiance au filtre.

Rôle
Découverte & rapport
Sévérité
High · 8.2
Advisory
11 juil. 2026
CVE
13 août 2026

01Contexte

SVGO est l'outil de référence pour optimiser des fichiers SVG en Node.js : il est intégré à de nombreux outils de build et pipelines d'images, avec un très grand nombre de téléchargements chaque semaine.

Parmi ses plugins, removeScripts a une promesse particulière : retirer d'un SVG tout ce qui peut exécuter du JavaScript. Concrètement, des applications s'en servent comme d'un filtre de sécurité, par exemple pour « nettoyer » les avatars ou logos SVG envoyés par leurs utilisateurs avant de les servir depuis leur propre domaine.

02Comment je l'ai trouvée

Je lisais le code de SVGO simplement pour comprendre comment le projet était construit. removeScripts a attiré mon attention pour deux raisons :

  • c'est l'un des seuls plugins qui fait une promesse de sécurité explicite ;
  • il est utilisé à très grande échelle, donc la moindre faille a un impact réel.

J'ai donc regardé précisément comment il reconnaît un script. Réponse : par correspondance de motifs (pattern matching). Il cherche des éléments <script> et des URI javascript:. Et les deux vérifications se sont révélées plus strictes que ce qu'un navigateur accepte réellement.

03Les deux contournements

a. Élément script préfixé par un namespace

Le plugin ne reconnaît que la balise nue <script>. Si on déclare un préfixe personnalisé pointant vers le namespace SVG, la balise passe intacte :

<svg xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg">
  <svg:script>alert(document.domain)</svg:script>
</svg>

b. Schéma javascript: sensible à la casse

Le filtre compare l'URI à la chaîne littérale en minuscules. Une variante avec des majuscules n'est pas détectée :

<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
  <a xlink:href="JavaScript:alert(document.domain)"><text>click</text></a>
</svg>

04Cause racine

Les deux bugs ont la même origine : un écart d'interprétation (parser differential) entre le filtre et le navigateur.

  • Namespaces XML : un navigateur identifie un élément par l'URI de son namespace, pas par la chaîne du préfixe. svg:script et script désignent donc exactement le même élément, et le navigateur l'exécute.
  • Schémas d'URI : la RFC 3986 définit le schéma comme insensible à la casse. JavaScript:, JAVASCRIPT: et javascript: sont équivalents pour le navigateur.
La leçon générale : une liste noire (blocklist) qui compare des chaînes de caractères doit raisonner exactement comme le parseur final. Sinon, chaque différence de normalisation (casse, namespace, encodage, espaces) devient un contournement.

05Impact

Un SVG « optimisé » contenant l'un de ces payloads exécute le script dès qu'il est ouvert directement ou intégré en ligne, sur l'origine qui le sert. Le scénario réaliste : une application accepte des SVG utilisateurs, les passe dans removeScripts en pensant les rendre sûrs, puis les sert depuis son propre domaine. Résultat : XSS stockée (stored XSS), avec accès à la session de la victime sur ce domaine.

Sévérité retenue : High, CVSS 8.2.

06Divulgation & correctifs

J'ai signalé les deux problèmes en privé aux mainteneurs, avec une preuve de concept testant les deux variantes. Les détails n'ont été publiés qu'une fois le correctif disponible (coordinated disclosure). Merci à SethFalco et à l'équipe SVGO pour leur réactivité.

BrancheVersions affectéesCorrigé en
v11.0.0 – 1.3.2jamais (dépréciée)
v22.0.0 – 2.8.22.8.3 (partiel*)
v33.0.0 – 3.3.33.3.4
v44.0.0 – 4.0.14.0.2

* La v2 n'a jamais filtré les URI javascript: : seul le contournement par namespace y est corrigé.

  1. 11 juillet 2026 : publication de l'advisory GHSA-2p49-hgcm-8545
  2. 13 août 2026 : attribution de l'identifiant CVE-2026-73650

07Ce que j'en retiens

  • Lire le code d'un filtre, c'est lire ses hypothèses. Chaque comparaison de chaîne est une question : « le navigateur raisonne-t-il pareil ? »
  • Liste blanche plutôt que liste noire. Garder uniquement les éléments et attributs connus comme sûrs est plus robuste que traquer les dangereux.
  • Défense en profondeur côté application : servir les fichiers utilisateurs depuis un domaine séparé, avec une Content-Security-Policy stricte, et utiliser un vrai sanitizer (ex. DOMPurify) plutôt qu'un optimiseur.
  • La divulgation, c'est aussi de la communication : un rapport clair, une PoC minimale et reproductible, et de la patience.