Skip to content
KitploitKITPLOIT
OutilsExploitsBlog
Log in
Soumettre
OutilsExploitsBlog
Soumettre

Outils de Hacking, PenTest et Cybersécurité pour votre Arsenal de Sécurité !

Kitploit est un répertoire d'outils de hacking, de cybersécurité et de pentesting. Découvrez les dernières mises à jour des projets pour trouver des vulnérabilités, analyser des systèmes, automatiser les tests et renforcer votre sécurité.

FluxContactConfidentialité© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
Instatic-Stored-XSS-CVE-2026-103931 — Preuve de concept et analyse d'un XSS stocké dans le filtre d'URL isSafeUrl() d'Instatic, où les caractères de contrôle C0 en tête contournent le blocage du schéma javascript:. | Kitploit
Outils/GitHubGitHub/overgrowncarrot1/instatic-stored-xss-cve-2026-103931
Analyse StatiqueAnalyse des VulnérabilitésAnalyse de CodeExploitationExploitation d'Applications WebSécurité Web
GitHubovergrowncarrot1/instatic-stored-xss-cve-2026-103931

Instatic-Stored-XSS-CVE-2026-103931

Preuve de concept et analyse d'un XSS stocké dans le filtre d'URL isSafeUrl() d'Instatic, où les caractères de contrôle C0 en tête contournent le blocage du schéma javascript:.

Voir le dépôt
il y a 1 jourPas encore vérifié

Populaires

Voir tout →

Découvrez les outils les plus utilisés par notre communauté.

Explorer tous les outils

Parcourez notre collection d'outils

Voir tous les outils →
Partager

Contournement du filtre de schéma d'URL dans isSafeUrl() via des caractères de contrôle C0 en tête

Affecté : Instatic v0.0.13 / commit 63ad5d6 (et toutes les révisions antérieures contenant src/core/html-sanitize/index.ts) Composant : src/core/html-sanitize/index.ts → isSafeUrl() / safeUrl() Classe : CWE-79 (XSS stocké) via CWE-184 (Liste incomplète d'entrées interdites)

Résumé

isSafeUrl() est le point de passage unique qui bloque les URL javascript:, vbscript: et data: dans l'ensemble du publisher. Il normalise l'entrée avec .replace(/[\t\n\r]/g, '').trim() avant de tester le préfixe du schéma.

Le parseur d'URL WHATWG supprime tous les caractères de contrôle C0 (U+0000–U+001F) et l'espace en tête avant de lire un schéma. Le String.prototype.trim() de JavaScript ne supprime que U+0009, U+000A, U+000B, U+000C, U+000D, U+0020 et les espaces Unicode — il laisse U+0000–U+0008 et U+000E–U+001F en place.

Ainsi, une URL préfixée par exemple par U+0001 est signalée comme sûre par le garde-fou, alors que tous les navigateurs la parsent et l'exécutent comme le schéma javascript:.

Preuve de concept

Contre le fichier src/core/html-sanitize/index.ts non modifié :

payload           : "\x01javascript:alert(document.domain)"
isSafeUrl()       : true          <-- le garde-fou signale "sûr"
WHATWG URL scheme : javascript:   <-- ce que le navigateur exécute réellement
safeUrl() output  : "\x01javascript:alert(document.domain)"   (PAS réduit à "#")

27 des 32 caractères de contrôle C0 contournent la vérification. U+0000 est neutralisé par le parsing des attributs HTML (NUL → U+FFFD), laissant 26 préfixes exploitables de manière fiable (U+0001–U+0008, U+000E–U+001F). Le même contournement met en échec les filtres vbscript: et data:.

La suite existante (src/__tests__/publisher/utils.test.ts) couvre le case folding et les tabulations intégrées (java\tscript:) mais jamais un caractère de contrôle en tête, ce qui explique pourquoi cela n'a pas été détecté.

Chemin de bout en bout

base.link déclare href: { type: 'url' } et LinkPropsSchema le type comme un Type.String({ default: '#' }) non contraint — il n'y a pas de validation d'URL à l'écriture. Par conséquent :

  1. escapeProps() route les props type: 'url' | 'image' | 'media' vers isSafeUrl(value) ? value : '#' — le payload passe brut et délibérément non échappé en HTML.
  2. Le render() de base.link émet `<a href="https://github.com/overgrowncarrot1/instatic-stored-xss-cve-2026-103931/blob/main/%24%7BsafeUrl%28props.href%29%7D" …>`. safeUrl() revérifie avec le même isSafeUrl() défectueux, puis escapeHtml() — qui n'échappe que & < > " ' et ne touche pas aux caractères de contrôle.
  3. Le payload atterrit verbatim dans l'attribut href : <a href="https://github.com/overgrowncarrot1/instatic-stored-xss-cve-2026-103931/blob/main/%5Cx01javascript%3Aalert%28document.domain%29" target="_self">Click me</a>

Sinks affectés

Tous ceux-ci passent par le même isSafeUrl() :

SinkFichier
Chaque prop de module url / image / media (lien href, bouton href, image src, vidéo src/poster, formulaire action, formulaire redirectUrl)src/core/publisher/escapeProps.ts:108
Attributs HTML personnalisés arbitraires définis par l'utilisateur sur n'importe quel nœudsrc/core/htmlAttributes/attributes.ts:66
href et src des liens/images Markdownsrc/core/markdown/renderMarkdown.ts:75
faviconUrl du sitesrc/core/publisher/render.ts:322
L'appel safeUrl() de chaque module de basesrc/modules/base/utils/escape.ts

Impact

Escalade de privilèges depuis un éditeur à faibles privilèges vers une compromission complète de l'admin.

Le rôle Client intégré détient site.content.edit, ce qui suffit pour définir un href de lien ou un attribut HTML personnalisé. L'URL injectée est ensuite rendue dans le canvas de l'éditeur admin, qui est une iframe srcdoc — de même origine que /admin.

server/securityHeaders.ts:69-72 ne définit que frame-ancestors 'none'; base-uri 'self'; object-src 'none' sur /admin, avec un commentaire de code explicite indiquant qu'une politique script-src n'est délibérément pas définie pour l'instant. Sans script-src, rien n'empêche une URL javascript: de s'exécuter sur l'origine admin.

Lorsqu'un Owner ou un Admin ouvre la page affectée dans l'éditeur et active l'élément, le payload s'exécute de même origine que la SPA admin. Le cookie de session est HttpOnly, il ne peut donc pas être lu directement — mais le payload peut piloter l'API admin en tant que victime (créer un compte owner, lire des secrets, ou installer un plugin, dont le point d'entrée serveur est un chemin vers l'exécution de code).

Sur le site publié, l'impact est plus restreint : cspPlan.ts définit script-src 'none' (ou 'self'), ce qui bloque les URL javascript:. Cependant server/publish/frontendInjections.ts:377 l'assouplit en 'self' 'unsafe-inline' pour toute page portant un script inline, et 'unsafe-inline' réautorise les URL javascript: — le XSS sur le site publié est donc atteignable sur ces pages.

Notez que la docstring de attributes.ts identifie déjà ce modèle de menace exact ("sur le site publié ET, plus grave, dans le canvas de l'éditeur admin (de même origine que /admin)") — le garde-fou ne l'implémente simplement pas complètement.

Vérification

Confirmé dans un navigateur (Chromium, iframes srcdoc reproduisant la CSP de chaque origine, rendant la sortie octet pour octet du safeUrl() du dépôt lui-même) :

Origine reproduiteCSP appliquéeRésultat
Canvas de l'éditeur admin (/admin)aucune — telle qu'émise par securityHeaders.tsjavascript: exécuté
Page publiée (référence)script-src 'none' — telle qu'émise par cspPlan.tsbloqué (script-src-elem)
Page publiée avec un script inlinescript-src 'self' 'unsafe-inline' — telle qu'émise par frontendInjections.ts:377voir note

Les deux premières lignes sont des résultats observés. L'exécution sur l'origine admin a rapporté document.domain comme l'origine servante, confirmant une exécution de même origine plutôt qu'un contexte opaque.

Réserve sur la fidélité : le harnais applique chaque politique via <meta http-equiv>, alors qu'Instatic l'envoie comme en-tête de réponse HTTP. Ces deux approches sont équivalentes pour l'application de script-src, mais une reproduction sur une instance bun run dev en direct porterait les en-têtes authentiques.

Correctif

Supprimer la plage complète C0 + espace au lieu de se fier à trim(). C'est précisément ce que fait la regex isJavaScriptProtocol de React elle-même avec son préfixe ^[\u0000-\u001F ]* — une vérification croisée utile montrant qu'il s'agit d'une classe de contournement connue et réelle plutôt que théorique.

--- a/src/core/html-sanitize/index.ts
+++ b/src/core/html-sanitize/index.ts
@@ -30,7 +30,11 @@ export function escapeHtml(value: unknown): string {
   * normalisation browsers apply during URL parsing.
   */
  export function isSafeUrl(url: string): boolean {
-  const normalized = url.replace(/[\t\n\r]/g, '').trim().toLowerCase()
+  const normalized = String(url ?? '')
+    .replace(/[\t\n\r]/g, '')
+    .replace(/^[\u0000-\u0020]+/, '')
+    .replace(/[\u0000-\u0020]+$/, '')
+    .toLowerCase()
    return (
      !normalized.startsWith('javascript:') &&
      !normalized.startsWith('vbscript:') &&

Vérifié contre le fichier corrigé : le payload est bloqué, safeUrl() le réduit à #, 0 des 99 URL dangereuses préfixées par un caractère de contrôle/espace restent acceptées, et les 14 cas de test isSafeUrl existants se comportent de manière identique.

Test de régression suggéré

Télécharger l’outil