Skip to content
KitploitKITPLOIT
ToolsBlog
Log in
Submit
ToolsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
CVE-2025-67923 — JetEngine <= 3.7.7 — Unauthenticated Stored Cross-Site Scripting via CCT REST API | Kitploit
Tools/GitHubGitHub/randomrobbiebf/cve-2025-67923
Vulnerability AnalysisExploitationWeb Application ExploitationWeb SecurityAPI Security
GitHubrandomrobbiebf/cve-2025-67923

CVE-2025-67923

JetEngine <= 3.7.7 — Unauthenticated Stored Cross-Site Scripting via CCT REST API

View Repository
196 months agoNot yet reviewed

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share

CVE-2025-67923 Exploitation Report

JetEngine <= 3.7.7 — Unauthenticated Stored Cross-Site Scripting via CCT REST API

Date: March 11, 2026 CVSS Score: 7.1 (High) CVSS Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:L Affected Plugin: JetEngine <= 3.7.7 Plugin Slug: jet-engine Fixed In: 3.7.8 CVE: CVE-2025-67923 CWE: CWE-79 Researcher: Bonds (via Patchstack) Reported: October 19, 2025 Disclosed: January 2026 WordPress Version Tested: Latest


Executive Summary

CVE-2025-67923 is an Unauthenticated Stored Cross-Site Scripting vulnerability in the JetEngine WordPress plugin affecting all versions up to and including 3.7.7. An unauthenticated attacker can write arbitrary HTML/JavaScript into a Custom Content Type (CCT) text field via the public REST API, which is then injected unsanitized into the DOM via a JavaScript innerHTML sink in the Maps Listing widget when a victim visits any page containing that widget.

The attack requires no authentication and no privileges. It requires only that:

  1. A JetEngine CCT exists with REST API write access set to public.
  2. A Maps Listing widget on any front-end page is configured to display that CCT's text field as the map marker label.

✅ VULNERABILITY CONFIRMED — Stored XSS Payload Written and Executed

Confirmed impact:

  • Unauthenticated arbitrary HTML/JS stored in CCT database without sanitization
  • XSS fires for every visitor loading any page with the Maps Listing widget
  • Cookie theft, session hijacking, stored credential harvesting, admin takeover

Vulnerability Details

Technical Summary

The vulnerability is a combination of two independent weaknesses:

  1. Missing input sanitization (store): The CCT item handler's sanitize_field_value() method has no sanitization path for text-type fields. The default: case only converts timestamps — raw HTML passes through and is persisted to the database.

  2. DOM-based XSS sink (render): The Maps Listing widget reads the stored field value via get_marker_label(), wraps it in htmlspecialchars(json_encode(...)) for the data-markers HTML attribute, and the JavaScript frontend reads the attribute with getAttribute() (which decodes HTML entities), then inserts markerData.label directly via innerHTML — executing any embedded HTML/JavaScript.

Affected Components

FileIssue
includes/modules/custom-content-types/inc/rest-api/public-controller.php:424create_item_permissions_check returns true when CCT rest_put_access = 'public' — no auth required
includes/modules/custom-content-types/inc/item-handler.php:489sanitize_field_value() default: branch — no HTML sanitization for text type fields
includes/modules/maps-listings/inc/render.php:233get_marker_label() returns raw meta value with no esc_html()
includes/modules/maps-listings/inc/render.php:247htmlspecialchars(json_encode($result)) — only protects attribute boundary, not innerHTML injection
includes/modules/maps-listings/assets/js/frontend-maps.js:112pinData.content = general.marker.html.replace('_marker_label_', markerData.label) — raw label inserted into HTML string
includes/modules/maps-listings/assets/js/public/mapbox-maps.js:175el.innerHTML = data.content — XSS execution sink
includes/modules/maps-listings/assets/js/public/leaflet-maps.js:34contentHtml.innerHTML = content — XSS execution sink

Exploit Chain

Step 1 — Unauthenticated REST Write

The CCT public REST controller checks permissions via check_user_permissions():

// 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' );
}

When a CCT is configured with rest_put_access = 'public' (a supported, documented configuration for public-facing forms), the endpoint is fully unauthenticated. Any HTTP client can POST to /wp-json/jet-cct/{slug}.

Step 2 — Unsanitized Storage

The REST handler calls $handler->update_item($params) which reaches sanitize_field_value():

// 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;
}

A text-type field hits the default: branch. maybe_to_timestamp() returns the value unchanged for non-date strings. The XSS payload `` is stored verbatim.

Step 3 — Label Retrieved Without Escaping

When the Maps Listing widget renders, get_marker_label() reads the field:

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

The returned value is placed into the marker data array:

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

Step 4 — Attribute Encoding Does Not Prevent innerHTML XSS

The marker array is encoded for an HTML attribute:

// render.php:247
return htmlspecialchars( json_encode( $result ) );

json_encode serialises `` as the string "". htmlspecialchars then HTML-encodes the full JSON, producing:

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

This is written to the data-markers HTML attribute. The encoding only protects the attribute boundary. When JavaScript reads the attribute, the browser decodes HTML entities, restoring the original characters:

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

Step 5 — innerHTML Injection (XSS Fires)

In frontend-maps.js, the raw label string is spliced directly into an HTML content string:

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

This content string is then set as the marker element's innerHTML:

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

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

Every visitor who loads a page containing the Maps Listing widget triggers the payload.


Proof of Concept

Prerequisites

Download Tool