weForms <= 1.6.27 - Authenticated (Subscriber+) Stored Cross-Site Scripting via Hidden Field Value via REST API
Description
The weForms plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the REST API entry submission endpoint in all versions up to, and including, 1.6.27. This is due to inconsistent input sanitization between the frontend AJAX handler and the REST API endpoint. When entries are submitted via the REST API (`/wp-json/weforms/v1/forms/{id}/entries/`), the `prepare_entry()` method in `class-abstract-fields.php` receives the WP_REST_Request object as `$args`, bypassing the `weforms_clean()` fallback that sanitizes `$_POST` data for frontend submissions. The base field handler only applies `trim()` to the value. This makes it possible for authenticated attackers, with Subscriber-level access and above, to inject arbitrary web scripts into form entry hidden field values via the REST API that execute when an administrator views the form entries page, where data is rendered using a Vue.js `v-html` directive without escaping.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:NTechnical Details
What Changed in the Fix
Changes introduced in v1.6.28
Source Code
WordPress.org SVNThis research plan outlines the steps required to exploit a stored Cross-Site Scripting (XSS) vulnerability in the weForms plugin via its REST API. ### 1. Vulnerability Summary The weForms plugin (<= 1.6.27) contains a stored XSS vulnerability because its REST API endpoint for form entry submission…
Show full research plan
This research plan outlines the steps required to exploit a stored Cross-Site Scripting (XSS) vulnerability in the weForms plugin via its REST API.
1. Vulnerability Summary
The weForms plugin (<= 1.6.27) contains a stored XSS vulnerability because its REST API endpoint for form entry submission (/wp-json/weforms/v1/forms/{id}/entries/) does not apply the same level of input sanitization as the frontend AJAX handler. Specifically, the prepare_entry() method in class-abstract-fields.php processes the WP_REST_Request data without invoking the weforms_clean() fallback used for $_POST data. For hidden fields, only a trim() is applied. The injected script is later rendered unescaped in the WordPress admin dashboard using Vue.js's v-html directive in assets/js-templates/spa-components.php.
2. Attack Vector Analysis
- Endpoint:
POST /wp-json/weforms/v1/forms/(?P<form_id>\d+)/entries/ - Vulnerable Method:
Weforms_Forms_Controller::save_entrycallingprepare_entry. - Payload Parameter: The payload is sent as a form field value (using the
meta_keyornameof a hidden field). - Authentication: Authenticated (Subscriber-level access or higher).
- Precondition: A form must exist that contains at least one field (ideally a hidden field, though other fields might also be affected if they lack specific sanitization).
3. Code Flow
- Entry Point: An authenticated user sends a POST request to
wp-json/weforms/v1/forms/{id}/entries/. - Route Registration:
includes/api/class-weforms-forms-controller.phpregisters this route with the callbacksave_entryand permission checksubmit_permissions_check. - Processing:
save_entry()iterates through the form fields and calls theprepare_entry()method on the corresponding field class (inheriting fromWeForms_Field_Contractinincludes/fields/class-abstract-fields.php). - Sanitization Failure: In version 1.6.27, while the AJAX handler uses
weforms_clean($_POST), the REST API path passes theWP_REST_Requestobject directly. The base implementation ofprepare_entryfor simple fields like "Hidden" only performs atrim()on the value. - Storage: The unsanitized value is stored in the
{prefix}_weforms_entrymetatable. - Sink: When an administrator navigates to weForms > [Form Name] > Entries, the plugin loads a Vue.js application.
- Execution: The template
tmpl-wpuf-component-tableinassets/js-templates/spa-components.phpcontains:<td v-for="(header, index) in columns"><span v-html="entry.fields[index]"></span></td>
Thev-htmldirective renders the unsanitized stored entry as raw HTML, executing any included scripts.
4. Nonce Acquisition Strategy
The REST API requires a standard WordPress REST nonce (wp_rest action) when authenticated via cookies.
- Identify Trigger: The nonce is typically localized for the WordPress dashboard.
- Extraction:
- Navigate to the WordPress dashboard as the Subscriber user.
- Use
browser_evalto extract the nonce from thewpApiSettingsobject. - JavaScript:
window.wpApiSettings?.nonceorwindow.weForms?.nonce.
- Alternative: If
wpApiSettingsis unavailable, navigate to a page where weForms is loaded (e.g., the contact page) and check for localized scripts.
5. Exploitation Strategy
- Step 1: Authenticate as a Subscriber user.
- Step 2: Obtain Form ID and Field Name.
- Retrieve form list via
GET /wp-json/weforms/v1/forms. - Identify a form and its hidden field name (e.g.,
hidden_1).
- Retrieve form list via
- Step 3: Obtain REST Nonce. Use
browser_evalas described above. - Step 4: Submit Malicious Entry.
- Method:
POST - URL:
http://vulnerable-wp.local/wp-json/weforms/v1/forms/{form_id}/entries/ - Headers:
Content-Type: application/jsonX-WP-Nonce: [EXTRACTED_NONCE]
- Body:
{ "hidden_field_name": "<img src=x onerror=alert('CVE-2026-2707')>" }
- Method:
- Step 5: Trigger XSS. Log in as Admin and navigate to
wp-admin/admin.php?page=weforms#/forms/{form_id}/entries.
6. Test Data Setup
- Create Subscriber:
wp user create attacker attacker@example.com --role=subscriber --user_pass=password - Create Form with Hidden Field:
- Use
wp post createto create awpuf_contact_formpost. - Create a child
wpuf_inputpost for the hidden field. - Easier alternative: Use
wp evalto programmatically create a form usingweforms()->form->create(...). - Field Data: Ensure the hidden field has
nameset to something identifiable (e.g.,test_hidden_field).
- Use
7. Expected Results
- The REST API should respond with
201 Createdor200 OK, indicating the entry was accepted. - The
meta_valuein theweforms_entrymetatable for that entry should contain the raw, unsanitized<img src=x onerror=...>string. - Upon Admin viewing the entries table, the browser should display an alert box.
8. Verification Steps
- Check Database:
wp db query "SELECT meta_value FROM wp_weforms_entrymeta WHERE meta_value LIKE '%onerror%';" - Examine Vue Template: Verify the presence of
v-htmlinassets/js-templates/spa-components.php(line ~105):<td v-for="(header, index) in columns"><span v-html="entry.fields[index]"></span></td>
9. Alternative Approaches
- Field Types: If "Hidden" fields are sanitized in some configurations, try "Text" fields or "Dropdown" fields, as
prepare_entry()is the base handler for multiple field types. - Payloads: Use
document.locationorfetch()to demonstrate data exfiltration (e.g., stealing the Admin's_wpnonceto perform a CSRF action). - Public Submission: Check if the form settings allow guest submissions. If so, the same REST endpoint might be exploitable without Subscriber authentication (though the CVE explicitly mentions PR:L).
Summary
The weForms plugin for WordPress is vulnerable to Stored Cross-Site Scripting because its REST API submission endpoint for form entries fails to apply the same sanitization as its frontend AJAX counterpart. Authenticated attackers (Subscriber+) can submit malicious scripts through form fields (such as hidden fields) via the REST API, which are then executed in the context of an administrator's browser when they view the form entries page.
Vulnerable Code
/* assets/js-templates/spa-components.php line 105 */ <td v-for="(header, index) in columns"><span v-html="entry.fields[index]"></span></td> --- /* includes/api/class-weforms-forms-controller.php */ register_rest_route( $this->namespace, '/' . $this->rest_base . '/(?P<form_id>[\d]+)' . '/entries/', [ 'args' => [ 'form_id' => [ 'description' => __( 'Unique identifier for the object', 'weforms' ), 'type' => 'integer', 'sanitize_callback' => 'absint', 'validate_callback' => [ $this, 'is_form_exists' ], 'required' => true, ], ], [ 'methods' => WP_REST_Server::CREATABLE, 'callback' => [ $this, 'save_entry' ], 'permission_callback' => [ $this, 'submit_permissions_check' ], ], [ 'methods' => WP_REST_Server::READABLE, 'callback' => [ $this, 'get_entry' ], 'permission_callback' => [ $this, 'get_item_permissions_check' ], 'args' => $this->get_collection_params(), ], ] );
Security Fix
@@ -1,6 +1,12 @@ 'use strict'; -var _typeof33 = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; +var _typeof34 = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; + +var _typeof33 = typeof Symbol === "function" && _typeof34(Symbol.iterator) === "symbol" ? function (obj) { + return typeof obj === "undefined" ? "undefined" : _typeof34(obj); +} : function (obj) { + return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj === "undefined" ? "undefined" : _typeof34(obj); +}; var _typeof32 = typeof Symbol === "function" && _typeof33(Symbol.iterator) === "symbol" ? function (obj) { return typeof obj === "undefined" ? "undefined" : _typeof33(obj);
Exploit Outline
The exploit involves an authenticated attacker with Subscriber-level privileges or higher using the plugin's REST API to submit a form entry. 1. The attacker authenticates to the WordPress site as a Subscriber user. 2. The attacker retrieves a valid REST API nonce (e.g., from the `wpApiSettings` object in the dashboard). 3. The attacker identifies a target Form ID and the name of a field (typically a hidden field which receives minimal processing). 4. The attacker sends a POST request to `/wp-json/weforms/v1/forms/{id}/entries/` with a payload containing a malicious script, such as `<img src=x onerror=alert(document.cookie)>`, in the field value. 5. The script is stored in the database without adequate sanitization because the REST controller bypasses the `weforms_clean` logic used by the AJAX submission handler. 6. When an administrator views the 'Entries' page for that form in the WordPress dashboard, the Vue.js application renders the stored field value using the `v-html` directive, causing the script to execute in the administrator's browser context.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.