CVE-2026-2707

weForms <= 1.6.27 - Authenticated (Subscriber+) Stored Cross-Site Scripting via Hidden Field Value via REST API

mediumImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
6.4
CVSS Score
6.4
CVSS Score
medium
Severity
1.6.28
Patched in
1d
Time to patch

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:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
Low
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=1.6.27
PublishedMarch 10, 2026
Last updatedMarch 11, 2026
Affected pluginweforms

What Changed in the Fix

Changes introduced in v1.6.28

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

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…

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_entry calling prepare_entry.
  • Payload Parameter: The payload is sent as a form field value (using the meta_key or name of 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

  1. Entry Point: An authenticated user sends a POST request to wp-json/weforms/v1/forms/{id}/entries/.
  2. Route Registration: includes/api/class-weforms-forms-controller.php registers this route with the callback save_entry and permission check submit_permissions_check.
  3. Processing: save_entry() iterates through the form fields and calls the prepare_entry() method on the corresponding field class (inheriting from WeForms_Field_Contract in includes/fields/class-abstract-fields.php).
  4. Sanitization Failure: In version 1.6.27, while the AJAX handler uses weforms_clean($_POST), the REST API path passes the WP_REST_Request object directly. The base implementation of prepare_entry for simple fields like "Hidden" only performs a trim() on the value.
  5. Storage: The unsanitized value is stored in the {prefix}_weforms_entrymeta table.
  6. Sink: When an administrator navigates to weForms > [Form Name] > Entries, the plugin loads a Vue.js application.
  7. Execution: The template tmpl-wpuf-component-table in assets/js-templates/spa-components.php contains:
    <td v-for="(header, index) in columns"><span v-html="entry.fields[index]"></span></td>
    The v-html directive 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.

  1. Identify Trigger: The nonce is typically localized for the WordPress dashboard.
  2. Extraction:
    • Navigate to the WordPress dashboard as the Subscriber user.
    • Use browser_eval to extract the nonce from the wpApiSettings object.
    • JavaScript: window.wpApiSettings?.nonce or window.weForms?.nonce.
  3. Alternative: If wpApiSettings is unavailable, navigate to a page where weForms is loaded (e.g., the contact page) and check for localized scripts.

5. Exploitation Strategy

  1. Step 1: Authenticate as a Subscriber user.
  2. 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).
  3. Step 3: Obtain REST Nonce. Use browser_eval as described above.
  4. Step 4: Submit Malicious Entry.
    • Method: POST
    • URL: http://vulnerable-wp.local/wp-json/weforms/v1/forms/{form_id}/entries/
    • Headers:
      • Content-Type: application/json
      • X-WP-Nonce: [EXTRACTED_NONCE]
    • Body:
      {
        "hidden_field_name": "<img src=x onerror=alert('CVE-2026-2707')>"
      }
      
  5. 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

  1. Create Subscriber: wp user create attacker attacker@example.com --role=subscriber --user_pass=password
  2. Create Form with Hidden Field:
    • Use wp post create to create a wpuf_contact_form post.
    • Create a child wpuf_input post for the hidden field.
    • Easier alternative: Use wp eval to programmatically create a form using weforms()->form->create(...).
    • Field Data: Ensure the hidden field has name set to something identifiable (e.g., test_hidden_field).

7. Expected Results

  • The REST API should respond with 201 Created or 200 OK, indicating the entry was accepted.
  • The meta_value in the weforms_entrymeta table 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

  1. Check Database:
    wp db query "SELECT meta_value FROM wp_weforms_entrymeta WHERE meta_value LIKE '%onerror%';"
    
  2. Examine Vue Template: Verify the presence of v-html in assets/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.location or fetch() to demonstrate data exfiltration (e.g., stealing the Admin's _wpnonce to 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).
Research Findings
Static analysis — not yet PoC-verified

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

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/weforms/1.6.27/assets/js/spa-app.js /home/deploy/wp-safety.org/data/plugin-versions/weforms/1.6.28/assets/js/spa-app.js
--- /home/deploy/wp-safety.org/data/plugin-versions/weforms/1.6.27/assets/js/spa-app.js	2025-12-18 17:15:54.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/weforms/1.6.28/assets/js/spa-app.js	2026-03-05 17:01:18.000000000 +0000
@@ -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.