Skip to content
KitploitKITPLOIT
HerramientasExploitsBlog
Log in
Enviar
HerramientasExploitsBlog
Enviar

¡Herramientas de Hacking, PenTest y Ciberseguridad para tu Arsenal de Seguridad!

Kitploit es un directorio de herramientas de hacking, ciberseguridad y pentesting. Descubre las últimas actualizaciones de proyectos para encontrar vulnerabilidades, analizar sistemas, automatizar pruebas y fortalecer tu seguridad.

FeedsContactoPrivacidad© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
Instatic-Stored-XSS-CVE-2026-103931 — Prueba de concepto y análisis de un XSS almacenado en el filtro de URL isSafeUrl() de Instatic, donde los caracteres de control C0 iniciales eluden el bloqueo del esquema javascript:. | Kitploit
Herramientas/GitHubGitHub/overgrowncarrot1/instatic-stored-xss-cve-2026-103931
Análisis EstáticoAnálisis de VulnerabilidadesAnálisis de CódigoExplotaciónExplotación de Aplicaciones WebSeguridad Web
GitHubovergrowncarrot1/instatic-stored-xss-cve-2026-103931

Instatic-Stored-XSS-CVE-2026-103931

Prueba de concepto y análisis de un XSS almacenado en el filtro de URL isSafeUrl() de Instatic, donde los caracteres de control C0 iniciales eluden el bloqueo del esquema javascript:.

Ver Repositorio
20hace 26 díasAún no revisado

Más Populares

Ver todos →

Descubre las herramientas más usadas por nuestra comunidad.

Explora todas las herramientas

Explora nuestra colección de herramientas

Ver todas las herramientas →
Compartir

Omisión del filtro de esquema de URL en isSafeUrl() mediante caracteres de control C0 iniciales

Afecta a: Instatic v0.0.13 / commit 63ad5d6 (y todas las revisiones anteriores que contengan src/core/html-sanitize/index.ts) Componente: src/core/html-sanitize/index.ts → isSafeUrl() / safeUrl() Clase: CWE-79 (XSS almacenado) mediante CWE-184 (Lista incompleta de entradas no permitidas)

Resumen

isSafeUrl() es el único punto de control que bloquea las URL javascript:, vbscript: y data: en todo el publicador. Normaliza la entrada con .replace(/[\t\n\r]/g, '').trim() antes de comprobar el prefijo del esquema.

El analizador de URL de WHATWG elimina todos los caracteres de control C0 iniciales (U+0000–U+001F) y el espacio antes de leer un esquema. El String.prototype.trim() de JavaScript solo elimina U+0009, U+000A, U+000B, U+000C, U+000D, U+0020 y los espacios Unicode — deja U+0000–U+0008 y U+000E–U+001F en su sitio.

Por lo tanto, una URL con el prefijo, por ejemplo, U+0001 es reportada como segura por el guardián, mientras que todos los navegadores la analizan y ejecutan como el esquema javascript:.

Prueba de concepto

Contra el src/core/html-sanitize/index.ts sin modificar:

payload           : "\x01javascript:alert(document.domain)"
isSafeUrl()       : true          <-- el guardián informa "seguro"
WHATWG URL scheme : javascript:   <-- lo que el navegador ejecuta realmente
safeUrl() output  : "\x01javascript:alert(document.domain)"   (NO colapsado a "#")

27 de los 32 caracteres de control C0 omiten la comprobación. U+0000 es neutralizado por el análisis de atributos HTML (NUL → U+FFFD), dejando 26 prefijos explotables de forma fiable (U+0001–U+0008, U+000E–U+001F). La misma omisión elude los filtros de vbscript: y data:.

La suite existente (src/__tests__/publisher/utils.test.ts) cubre el plegado de mayúsculas y las tabulaciones incrustadas (java\tscript:) pero nunca un carácter de control inicial, que es la razón por la que esto no se detectó.

Ruta de extremo a extremo

base.link declara href: { type: 'url' } y LinkPropsSchema lo tipa como un Type.String({ default: '#' }) sin restricciones — no hay validación de URL en el momento de la escritura. Por lo tanto:

  1. escapeProps() enruta las props type: 'url' | 'image' | 'media' a isSafeUrl(value) ? value : '#' — el payload pasa en bruto y deliberadamente sin escape HTML.
  2. El render() de base.link emite `<a href="https://github.com/overgrowncarrot1/instatic-stored-xss-cve-2026-103931/blob/main/%24%7BsafeUrl%28props.href%29%7D" …>`. safeUrl() vuelve a comprobar con el mismo isSafeUrl() roto, y luego escapeHtml() — que solo escapa & < > " ' y no toca los caracteres de control.
  3. El payload aterriza textualmente en el atributo 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>

Receptores afectados

Todos estos canalizan a través del mismo isSafeUrl():

ReceptorArchivo
Cada prop de módulo url / image / media (enlace href, botón href, imagen src, vídeo src/poster, formulario action, formulario redirectUrl)src/core/publisher/escapeProps.ts:108
Atributos HTML personalizados arbitrarios establecidos por el usuario en cualquier nodosrc/core/htmlAttributes/attributes.ts:66
href y src de enlaces/imágenes de Markdownsrc/core/markdown/renderMarkdown.ts:75
faviconUrl del sitiosrc/core/publisher/render.ts:322
La llamada safeUrl() de cada módulo basesrc/modules/base/utils/escape.ts

Impacto

Escalada de privilegios desde un editor con pocos privilegios hasta el compromiso total del administrador.

El rol integrado Client posee site.content.edit, lo cual es suficiente para establecer el href de un enlace o un atributo HTML personalizado. La URL inyectada se renderiza entonces en el lienzo del editor de administración, que es un iframe srcdoc — del mismo origen que /admin.

server/securityHeaders.ts:69-72 establece solo frame-ancestors 'none'; base-uri 'self'; object-src 'none' en /admin, con un comentario explícito en el código de que una política script-src deliberadamente no se establece todavía. Sin script-src, nada impide que una URL javascript: se ejecute en el origen de administración.

Cuando un Owner o Admin abre la página afectada en el editor y activa el elemento, el payload se ejecuta en el mismo origen que la SPA de administración. La cookie de sesión es HttpOnly, por lo que no se puede leer directamente — pero el payload puede manejar la API de administración como la víctima (crear una cuenta de owner, leer secretos o instalar un plugin, cuyo punto de entrada del servidor es una vía hacia la ejecución de código).

En el sitio publicado el impacto es más limitado: cspPlan.ts establece script-src 'none' (o 'self'), lo que bloquea las URL javascript:. Sin embargo, server/publish/frontendInjections.ts:377 lo relaja a 'self' 'unsafe-inline' para cualquier página que lleve un script en línea, y 'unsafe-inline' vuelve a permitir las URL javascript: — por lo que el XSS en el sitio publicado es alcanzable en esas páginas.

Nótese que el docstring de attributes.ts ya identifica este modelo de amenaza exacto ("en el sitio publicado Y, más gravemente, dentro del lienzo del editor de administración (mismo origen que /admin)") — el guardián simplemente no lo implementa por completo.

Verificación

Confirmado en un navegador (Chromium, iframes srcdoc reproduciendo la CSP de cada origen, renderizando la salida byte a byte del propio safeUrl() del repositorio):

Origen reproducidoCSP aplicadaResultado
Lienzo del editor de administración (/admin)ninguna — tal como emite securityHeaders.tsjavascript: ejecutado
Página publicada (línea base)script-src 'none' — tal como emite cspPlan.tsbloqueado (script-src-elem)
Página publicada con un script en líneascript-src 'self' 'unsafe-inline' — tal como emite frontendInjections.ts:377ver nota

Las dos primeras filas son resultados observados. La ejecución en el origen de administración reportó document.domain como el origen servidor, confirmando la ejecución en el mismo origen en lugar de un contexto opaco.

Advertencia sobre la fidelidad: el arnés aplica cada política mediante <meta http-equiv>, mientras que Instatic la envía como cabecera de respuesta HTTP. Son equivalentes para la aplicación de script-src, pero una reproducción en una instancia real de bun run dev llevaría las cabeceras genuinas.

Corrección

Eliminar el rango completo C0 + espacio en lugar de confiar en trim(). Esto es precisamente lo que hace la propia regex isJavaScriptProtocol de React con su prefijo ^[\u0000-\u001F ]* — una comprobación cruzada útil de que esta es una clase de omisión conocida y del mundo real, más que teórica.

--- 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:') &&

Verificado contra el archivo parcheado: el payload se bloquea, safeUrl() lo colapsa a #, 0 de 99 URL peligrosas con prefijo de control/espacio siguen siendo aceptadas, y los 14 casos de prueba existentes de isSafeUrl se comportan de forma idéntica.

Descargar herramienta