
JetEngine <= 3.7.7 — Nicht authentifiziertes gespeichertes Cross-Site-Scripting über die CCT REST API
Datum: 11. März 2026 CVSS-Score: 7.1 (Hoch) CVSS-Vektor: CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:L Betroffenes Plugin: JetEngine <= 3.7.7 Plugin-Slug: jet-engine Behoben in: 3.7.8 CVE: CVE-2025-67923 CWE: CWE-79 Forscher: Bonds (über Patchstack) Gemeldet: 19. Oktober 2025 Offengelegt: Januar 2026 Getestete WordPress-Version: Aktuellste
CVE-2025-67923 ist eine Schwachstelle für nicht authentifiziertes gespeichertes Cross-Site-Scripting im JetEngine-WordPress-Plugin, die alle Versionen bis einschließlich 3.7.7 betrifft. Ein nicht authentifizierter Angreifer kann über die öffentliche REST-API beliebiges HTML/JavaScript in ein Textfeld eines Custom Content Type (CCT) schreiben. Dieses wird anschließend über eine innerHTML-Senke im JavaScript des Maps-Listing-Widgets unbereinigt in das DOM injiziert, wenn ein Opfer eine Seite besucht, die dieses Widget enthält.
Der Angriff erfordert keine Authentifizierung und keine Berechtigungen. Er erfordert lediglich Folgendes:
public gesetzt ist.✅ SCHWACHSTELLE BESTÄTIGT — Gespeicherte XSS-Payload geschrieben und ausgeführt
Bestätigte Auswirkungen:
Die Schwachstelle ist eine Kombination aus zwei unabhängigen Fehlern:
Fehlende Eingabebereinigung (Speicherung): Die Methode sanitize_field_value() des CCT-Item-Handlers besitzt keinen Bereinigungspfad für Felder vom Typ text. Der default:-Fall konvertiert nur Zeitstempel — rohes HTML passiert unverändert und wird in der Datenbank gespeichert.
DOM-basierte XSS-Senke (Rendering): Das Maps-Listing-Widget liest den gespeicherten Feldwert über get_marker_label(), kapselt ihn für das HTML-Attribut data-markers in htmlspecialchars(json_encode(...)), und das JavaScript-Frontend liest das Attribut mit getAttribute() (das HTML-Entitäten dekodiert) und fügt markerData.label anschließend direkt über innerHTML ein — wodurch eingebettetes HTML/JavaScript ausgeführt wird.
| Datei | Problem |
|---|---|
includes/modules/custom-content-types/inc/rest-api/public-controller.php:424 | create_item_permissions_check gibt true zurück, wenn CCT rest_put_access = 'public' — keine Authentifizierung erforderlich |
includes/modules/custom-content-types/inc/item-handler.php:489 | default:-Zweig von sanitize_field_value() — keine HTML-Bereinigung für text-Felder |
includes/modules/maps-listings/inc/render.php:233 | get_marker_label() gibt rohen Metawert ohne esc_html() zurück |
includes/modules/maps-listings/inc/render.php:247 | htmlspecialchars(json_encode($result)) — schützt nur die Attributgrenze, keine innerHTML-Injektion |
includes/modules/maps-listings/assets/js/frontend-maps.js:112 | pinData.content = general.marker.html.replace('_marker_label_', markerData.label) — rohe Beschriftung wird in HTML-String eingefügt |
includes/modules/maps-listings/assets/js/public/mapbox-maps.js:175 | el.innerHTML = data.content — XSS-Ausführungssenke |
includes/modules/maps-listings/assets/js/public/leaflet-maps.js:34 | contentHtml.innerHTML = content — XSS-Ausführungssenke |
Der öffentliche REST-Controller des CCT prüft Berechtigungen über 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' );
}
Wenn ein CCT mit rest_put_access = 'public' konfiguriert ist (eine unterstützte, dokumentierte Konfiguration für öffentlich zugängliche Formulare), ist der Endpunkt vollständig ohne Authentifizierung erreichbar. Jeder HTTP-Client kann eine POST-Anfrage an /wp-json/jet-cct/{slug} senden.
Der REST-Handler ruft $handler->update_item($params) auf, wodurch sanitize_field_value() erreicht wird:
// 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;
}
Ein Feld vom Typ text erreicht den default:-Zweig. maybe_to_timestamp() gibt den Wert bei Nicht-Datum-Strings unverändert zurück. Die XSS-Payload `` wird unverändert gespeichert.
Wenn das Maps-Listing-Widget rendert, liest get_marker_label() das Feld:
// 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 ""
}
Der zurückgegebene Wert wird in das Marker-Daten-Array eingefügt:
// render.php:231-237
$result[] = array(
'id' => $post_id,
'latLang' => $latlang,
'label' => $this->get_marker_label( $post, $settings ), // raw XSS payload
// ...
);
Das Marker-Array wird für ein HTML-Attribut kodiert:
// render.php:247
return htmlspecialchars( json_encode( $result ) );
json_encode serialisiert `` als den String "". htmlspecialchars kodiert anschließend das gesamte JSON als HTML und erzeugt:
[{...,"label":"<img src=x onerror=alert(1)>",...}]
Dies wird in das HTML-Attribut data-markers geschrieben. Die Kodierung schützt nur die Attributgrenze. Wenn JavaScript das Attribut liest, dekodiert der Browser die HTML-Entitäten und stellt die ursprünglichen Zeichen wieder her:
// Browser automatically decodes entities when reading via dataset / getAttribute
const markers = JSON.parse(el.dataset.markers);
// markers[0].label === ''
In frontend-maps.js wird der rohe Beschriftungs-String direkt in einen HTML-Inhaltsstring eingefügt: