Registration Form Fields for WooCommerce: Wie Konten ohne Freigabe an der Admin-Prüfung vorbeikamen
Im WooCommerce-Shop eines Kunden tauchten Kundenkonten auf, die niemand freigegeben hatte. Sie hatten eine E-Mail-Adresse und sonst nichts: keinen Namen, keine Firma, keine Adresse, obwohl das Registrierungsformular all das verlangt. Trotzdem hatten sie schon die Rolle Kunde und konnten sich einloggen. Der Shop nutzt Registration Form Fields for WooCommerce mit Admin-Freigabe. Das hätte also nicht gehen dürfen.
Die Ursache war eine Lücke zwischen Plugin und WooCommerce. Das Plugin setzt seine Regeln nur durch, wenn seine eigenen versteckten Formularfelder in der Anfrage stehen. Der eingebaute Registrierungs-Handler von WooCommerce akzeptiert aber weiterhin eine einfache Anfrage ohne diese Felder, und das Plugin hat sie ungeprüft durchgelassen.
Ich habe das am 22. Oktober 2026 an den Hersteller WPEverest gemeldet. Version 2.1.3 behebt die Lücke, aktuell ist 2.1.4. Wenn du das Plugin nutzt: updaten. Der Rest des Beitrags erklärt, was schiefgelaufen ist, wie du prüfst, ob dein Shop betroffen war, und was du tun kannst, wenn du nicht sofort updaten kannst.
Wer betroffen ist
| Plugin | Registration Form Fields for WooCommerce (Slug custom-registration-form-fields-builder-for-woocommerce), vertrieben über den WooCommerce.com Marketplace |
| Hersteller | WPEverest (auf WooCommerce.com und im Plugin-Header als ThemeGrill geführt) |
| Nachweislich verwundbar | 2.1.2 |
| Ältere Versionen | Wahrscheinlich betroffen (gleicher Code-Pfad), nicht getestet |
| Behoben in | 2.1.3 (aktuell auf WooCommerce.com: 2.1.4) |
| Voraussetzung | Ein Registrierungsformular ist als Mein-Konto-Formular eingestellt, die normale Einrichtung des Plugins |
| Nicht nötig | Die WooCommerce-Einstellung “Kunden erlauben, auf der Seite Mein Konto ein Konto zu erstellen”; der Angriff funktioniert auch, wenn sie aus ist |
Am härtesten trifft es Shops, die das Formular mit Admin-Freigabe oder E-Mail-Bestätigung nutzen. Genau dieser Schritt fällt weg.
Wie Plugin und WooCommerce zusammenspielen
Das Plugin hat keinen eigenen Registrierungs-Endpunkt. Es ersetzt das Formular auf der Mein-Konto-Seite und überlässt das Anlegen des Kontos WooCommerce.
- WooCommerce verarbeitet die Registrierung in
WC_Form_Handler::process_registration(), eingehängt inwp_loaded. Das läuft auf jeder URL für jeden POST, derregister,emailund eine gültigewoocommerce-register-nonceenthält. Fehlt das Passwort, erzeugt WooCommerce eins. Nach dem Anlegen loggt es den Nutzer ein. - Das Plugin ergänzt seine Formularfelder und zwei versteckte Inputs:
tgwcfb-register-nonceundtgwcfb_id(die Post-ID des Formulars). Inwoocommerce_register_postprüft es die Felder und das Captcha und setzt bei Formularen mit Admin-Freigabe oder E-Mail-Bestätigung das User-Metauser_status = pending. Für diese Formulare schaltet es außerdem den automatischen Login nach der Registrierung ab. - Beim Login lehnt
UserApproval::check_status_on_login()nurpendingunddeniedab. Ein Nutzer ganz ohneuser_statusgilt als freigegeben, und auch die Benutzerliste im Admin zeigt ihn als freigegeben an.
Ein Detail ist noch wichtig: WordPress-Nonces für nicht eingeloggte Besucher sind für alle Besucher gleich und 12 bis 24 Stunden gültig. Beide Nonces stehen auf der öffentlichen Mein-Konto-Seite.
Die Lücke: eine einfache WooCommerce-Registrierung
Eine Registrierungsanfrage ohne die beiden versteckten Inputs des Plugins erzeugt einen eingeloggten Kunden ohne Freigabestatus. Jede Prüfung des Plugins steht hinter diesem frühen Return in FormHandler::validate_fields() (2.1.2):
if (
! isset( $_POST['tgwcfb-register-nonce'] ) ||
! wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['tgwcfb-register-nonce'] ) ), 'tgwcfb-register' ) ||
! isset( $_POST['tgwcfb_id'] ) ||
is_checkout()
) {
return;
}
Fehlt tgwcfb-register-nonce in der Anfrage, kehrt die Funktion zurück, ohne einen Fehler zu setzen. WooCommerce sieht keinen Fehler und legt das Konto an. Die Folgen:
- Pflichtfelder (Name, Firma, Adresse) werden nie geprüft
- das Captcha wird nie geprüft
user_statuswird nie gesetzt, das Konto gilt also als freigegeben- der Login nach der Registrierung bleibt aktiv, die Antwort enthält also schon eine gültige Session
Die Anfrage braucht nur Felder, die auf der Mein-Konto-Seite öffentlich sind:
POST /my-account/
email=<beliebige Adresse>
woocommerce-register-nonce=<Wert von der öffentlichen Seite>
register=Register
Ich habe das auf einer lokalen Kopie des betroffenen Shops nachgestellt (WordPress, WooCommerce, das Plugin und die Daten des Shops). Im Live-Shop waren mehrere Konten genau so angelegt worden, mit nichts als einer E-Mail-Adresse.
Der erste Fix reichte nicht
Der erste Fix-Kandidat des Herstellers verlangte die Nonce des Plugins, akzeptierte dann aber jede tgwcfb_id, die auf einen Formular-Post mit Inhalt zeigte. Ob das das eingestellte Formular war oder ob es veröffentlicht war, prüfte er nicht.
// erster Fix-Kandidat, vereinfacht
if ( ! wp_verify_nonce( $nonce, 'tgwcfb-register' ) || empty( get_blocks( $form_id ) ) ) {
$errors->add( ... );
}
get_blocks() prüft nur den Post-Typ. Ein Formular im Papierkorb, ein Entwurf oder ein altes Formular kommt also durch. Im betroffenen Shop lag das ursprüngliche Standardformular im Papierkorb. Es fragte nur Benutzername, E-Mail und Passwort ab und hatte keine Freigabe-Einstellung. Mit seiner ID entstand ein Konto ohne user_status, das sich einloggen konnte. Also dasselbe Ergebnis wie vorher.
Formular-IDs sind nicht geheim. Es sind fortlaufende Post-IDs, und die Fehlermeldung bei einer falschen ID unterscheidet sich von der Erfolgsantwort. Ein Bot findet eine passende ID also durch Hochzählen. Ich habe das vor dem finalen Release an den Hersteller gemeldet, der finale Build schließt die Lücke.
Auswirkung und Schweregrad
Der Angreifer bekommt ein Kundenkonto, das der Shopbetreiber nie freigegeben hat. Was dieses Konto erreichen kann, hängt vom Shop ab. Wenn die Freigabe Preise, Bestellungen, Downloads oder einen B2B-Katalog schützt, ist dieser Schutz weg. In jedem Shop filtern Pflichtfelder und Captcha keine Bots mehr, Spam-Konten sammeln sich an.
Das Konto bekommt die Rolle, die das Formular vergeben würde (standardmäßig customer). Eine höhere Rolle kann der Angreifer so nicht wählen, und bestehende Daten werden weder gelesen noch verändert.
Meine eigene Einschätzung ist CVSS 3.1 AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N, Basiswert 5.3 (Medium): eine einzige HTTP-Anfrage ohne Login, alle Eingaben auf einer öffentlichen Seite, keine Daten durch die Lücke selbst offengelegt, aber ein Zugriffsschutz umgangen. Shops, in denen die Freigabe sensible Inhalte schützt, bewerten das womöglich höher. Ob eine CVE beantragt wird, ist noch offen.
Was 2.1.3 ändert
Version 2.1.3 ergänzt FormHandler::require_registration_form() am WooCommerce-Filter woocommerce_process_registration_errors. Ist ein Mein-Konto-Formular eingestellt, muss jede Registrierung eine gültige Plugin-Nonce und eine Formular-ID enthalten, die die Seite tatsächlich anbietet:
- das eingestellte Mein-Konto- oder Checkout-Formular, unabhängig von seinem Status, oder
- ein veröffentlichtes Formular, das per Shortcode
[tgwcfb_registration_form]in veröffentlichten Inhalten oder einem Widget eingebunden ist.
Alles andere wird mit “Please register using the registration form.” abgelehnt. Die Liste der angebotenen Formulare liegt in einem Transient und wird geleert, sobald sich Beiträge, Widgets oder die beiden Formular-Einstellungen ändern. Bindet ein Page Builder dein Formular so ein, dass das Plugin es nicht erkennt, kannst du seine ID über den Filter tgwcfb_registration_form_ids ergänzen.
Ist kein Mein-Konto-Formular eingestellt, macht die Prüfung nichts. Das ist so gewollt: Dann ist das Plugin nicht für die Registrierung zuständig.
Ich habe den finalen Build des Herstellers auf der lokalen Kopie des Shops getestet, ohne meine eigenen Schutzmaßnahmen:
| Anfrage | Ergebnis |
|---|---|
| Einfache WooCommerce-Registrierung (die Lücke) | Abgelehnt |
| Eingestelltes Formular, alle Felder | Konto als pending angelegt, Login mit Passwort abgelehnt |
| ID des Standardformulars im Papierkorb, wenige Felder (Lücke im ersten Fix) | Abgelehnt |
| ID des Standardformulars im Papierkorb, alle Felder | Abgelehnt |
| ID einer privaten Seite, kein Formular | Abgelehnt |
| Formular-ID, die es nicht gibt | Abgelehnt |
Dieser Build trug im Plugin-Header noch die Version 2.1.2. Prüf nach dem Update, ob in deiner Plugin-Liste mindestens 2.1.3 steht.
Wenn du noch nicht updaten kannst
Bis zum Update schließt dieses Must-Use-Plugin die Lücke. Speicher es als PHP-Datei in wp-content/mu-plugins/. Es lässt nur Registrierungen mit der Nonce des Plugins und der ID des eingestellten Mein-Konto-Formulars durch. Ist das Plugin deaktiviert, blockiert es die Registrierung über Mein Konto komplett, statt sie offen zu lassen. Nach dem Update kann es bleiben.
<?php
/**
* Plugin Name: Enforce Registration Form
* Description: Rejects My Account registrations that do not come from the configured registration form.
*/
defined( 'ABSPATH' ) || exit;
add_filter(
'woocommerce_process_registration_errors',
function ( $errors ) {
$nonce = isset( $_POST['tgwcfb-register-nonce'] ) ? sanitize_text_field( wp_unslash( $_POST['tgwcfb-register-nonce'] ) ) : '';
$form_id = isset( $_POST['tgwcfb_id'] ) ? absint( wp_unslash( $_POST['tgwcfb_id'] ) ) : 0;
if ( ! wp_verify_nonce( $nonce, 'tgwcfb-register' ) || absint( get_option( '_tgwcfb_form_id' ) ) !== $form_id ) {
$errors->add( 'registration_form_required', 'Please register using the registration form.' );
}
return $errors;
}
);
Nutzt dein Shop ein separates Checkout-Formular, nimm _tgwcfb_checkout_form_id in die Prüfung mit auf.
War mein Shop betroffen?
Such nach Kunden, die nach dem Livegang des Formulars registriert wurden und weder user_status noch tgwcfb_form_id als User-Meta haben. Typisch sind Konten mit nur einer E-Mail-Adresse und ohne Namen. Prüf jedes Konto einzeln, bevor du es löschst, denn auch von Admins oder im Checkout angelegte Konten passen auf das Muster.
SELECT u.ID, u.user_login, u.user_email, u.user_registered
FROM wp_users u
WHERE NOT EXISTS (SELECT 1 FROM wp_usermeta m WHERE m.user_id = u.ID AND m.meta_key = 'tgwcfb_form_id')
AND NOT EXISTS (SELECT 1 FROM wp_usermeta m WHERE m.user_id = u.ID AND m.meta_key = 'user_status')
AND EXISTS (SELECT 1 FROM wp_usermeta m WHERE m.user_id = u.ID AND m.meta_key = 'wp_capabilities' AND m.meta_value LIKE '%"customer"%')
ORDER BY u.user_registered DESC;
Passe das Tabellenpräfix wp_ an dein eigenes an.
Konten, die über die ID eines anderen Formulars entstanden sind (die Lücke im ersten Fix), findet diese Abfrage nicht. Sie haben tgwcfb_form_id, aber kein user_status. Lass dir deshalb zusätzlich Kunden anzeigen, deren tgwcfb_form_id nicht dein eingestelltes Formular ist.
Ablauf der Meldung
| Datum | Ereignis |
|---|---|
| 02.11.2026 | Hersteller schickt den überarbeiteten Fix; ich bestätige, dass er beide Lücken schließt. Hersteller stimmt der Veröffentlichung zu |
| 29.10.2026 | Ich stelle fest, dass der erste Fix unvollständig ist (die ID eines Formulars im Papierkorb wird noch akzeptiert), und melde es |
| 27.10.2026 | Hersteller schickt den ersten Fix-Kandidaten |
| 22.10.2026 | Entdeckt bei der Suche nach unerwarteten Kundenkonten in einem Kundenshop; Meldung an WPEverest / ThemeGrill mit Ursache und Proof of Concept |
Elf Tage von der Meldung bis zum verifizierten Fix. Danke an das Team von WPEverest für den Fix und die Zustimmung zur Veröffentlichung.