CVE-2026-42658

Classified Listing – AI-Powered Classified ads & Business Directory Plugin <= 5.3.8 - Unauthenticated Stored Cross-Site Scripting

highImproper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
7.2
CVSS Score
7.2
CVSS Score
high
Severity
5.3.9
Patched in
6d
Time to patch

Description

The Classified Listing – AI-Powered Classified ads & Business Directory Plugin plugin for WordPress is vulnerable to Stored Cross-Site Scripting in versions up to, and including, 5.3.8 due to insufficient input sanitization and output escaping. This makes it possible for unauthenticated attackers 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:N/UI:N/S:C/C:L/I:L/A:N
Attack Vector
Network
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
Changed
Low
Confidentiality
Low
Integrity
None
Availability

Technical Details

Affected versions<=5.3.8
PublishedApril 29, 2026
Last updatedMay 4, 2026
Affected pluginclassified-listing

What Changed in the Fix

Changes introduced in v5.3.9

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

### 1. Vulnerability Summary The **Classified Listing** plugin (versions <= 5.3.8) contains an unauthenticated stored cross-site scripting (XSS) vulnerability. The vulnerability exists within the listing submission and update logic handled via AJAX. Specifically, the `FormBuilderAjax::update_listing…

Show full research plan

1. Vulnerability Summary

The Classified Listing plugin (versions <= 5.3.8) contains an unauthenticated stored cross-site scripting (XSS) vulnerability. The vulnerability exists within the listing submission and update logic handled via AJAX. Specifically, the FormBuilderAjax::update_listing() function processes user-supplied data from the formData parameter using parse_str() but fails to adequately sanitize this data before storing it in the database (either as post content or post metadata). Because this action is available to unauthenticated users (when "Post as Guest" is enabled), an attacker can inject malicious scripts into a listing. These scripts execute in the context of any user (including administrators) who views the affected listing on the frontend or backend.

2. Attack Vector Analysis

  • AJAX Action: rtcl_update_listing (available via wp_ajax_nopriv_rtcl_update_listing)
  • Vulnerable Parameter: formData
  • Authentication Required: None (unauthenticated), provided the "Post for unregistered user" setting is enabled.
  • Preconditions:
    1. The plugin setting post_for_unregister must be enabled (General Settings -> Moderation).
    2. A valid Form ID (form_id) must be known (typically 1 by default).

3. Code Flow

  1. Entry Point: app/Controllers/Ajax/FormBuilderAjax.php registers the AJAX actions:
    add_action( 'wp_ajax_nopriv_rtcl_update_listing', [ $this, 'update_listing' ] );
    
  2. Nonce Verification: The function checks for a nonce using rtcl()->nonceId (which is rtcl_nonce) and rtcl()->nonceText (which is rtcl_ajax_nonce):
    if ( ! wp_verify_nonce( isset( $_REQUEST[ rtcl()->nonceId ] ) ? $_REQUEST[ rtcl()->nonceId ] : null, rtcl()->nonceText ) ) { ... }
    
  3. Data Parsing: The formData string (containing the listing details) is parsed into an array:
    if ( ! empty( $_POST['formData'] ) ) {
        parse_str( $_POST['formData'], $formData );
    }
    
  4. Registration/User Logic: If unauthenticated, it optionally registers a user based on the email field in $formData.
  5. Storage (Sink): The parsed $formData is used to create or update a listing. While truncated in the source, the plugin uses wp_insert_post() and update_post_meta() to save fields like rtcl_title, rtcl_content, and custom field metadata. Because parse_str is used on raw input and subsequent validation in FBHelper::formDataValidation (inferred) focuses on structure rather than script neutralization, malicious HTML tags are stored in the database.
  6. Execution: When the listing is rendered via a template (e.g., single listing page), the unsanitized metadata or content is printed to the page.

4. Nonce Acquisition Strategy

The rtcl_update_listing action requires a valid WordPress nonce. This nonce is localized for the frontend listing submission form.

  1. Identify Shortcode: The listing form is rendered using the [rtcl_post_form] shortcode.
  2. Setup Page: Create a public page containing this shortcode to force the plugin to enqueue its scripts and localizations.
  3. Navigate: Use the browser_navigate tool to visit this page.
  4. Extract Nonce: The nonce is stored in the global JavaScript object rtcl_common. Use browser_eval to retrieve it.
    • JS Variable: rtcl_common.nonce
    • Nonce ID (Parameter name): rtcl_nonce

5. Exploitation Strategy

Step 1: Enable Unauthenticated Posting
Configure the plugin to allow guests to post listings without an account.

Step 2: Create a Form Page
Create a page with the [rtcl_post_form] shortcode to expose the necessary AJAX nonce.

Step 3: Extract the Nonce
Navigate to the newly created page and extract rtcl_common.nonce.

Step 4: Execute the Exploit
Send a POST request to admin-ajax.php to create a new listing with an XSS payload.

  • Endpoint: /wp-admin/admin-ajax.php
  • Method: POST
  • Content-Type: application/x-www-form-urlencoded
  • Parameters:
    • action: rtcl_update_listing
    • rtcl_nonce: [EXTRACTED_NONCE]
    • formId: 1 (or the ID of the active listing form)
    • formData: A URL-encoded string containing:
      • rtcl_title: XSS Test <img src=x onerror=alert(document.domain)>
      • rtcl_content: Malicious Listing Content
      • email: attacker@example.com (required for guest posting)
      • _rtcl_phone: <script>alert('Stored_XSS_Phone')</script> (often unescaped in listing details)

6. Test Data Setup

  1. Enable Guest Posting:
    # Update settings to enable post_for_unregister and set new listings to publish immediately
    wp option patch update rtcl_general_settings moderation post_for_unregister 1
    wp option patch update rtcl_general_settings moderation new_listing_status publish
    
  2. Identify/Create Form: Ensure a form exists.
    wp post list --post_type=rtcl_cf_group
    
  3. Create Form Page:
    wp post create --post_type=page --post_title="Submit Listing" --post_status=publish --post_content='[rtcl_post_form]'
    

7. Expected Results

  • The AJAX response should return {"success": true, ...} and a listingId.
  • A new post of type rtcl_listing will be created.
  • When visiting the listing URL (e.g., /?post_type=rtcl_listing&p=[ID]), the browser will execute the injected JavaScript (alert box).

8. Verification Steps

  1. Check Post Creation:
    wp post list --post_type=rtcl_listing --fields=ID,post_title,post_status
    
  2. Inspect Metadata for Payload:
    # Replace [ID] with the ID from the AJAX response
    wp post meta list [ID]
    
  3. Check for Sanitize Failures: Verify if the payload in post_title or _rtcl_phone remains exactly as sent (e.g., includes <script> or onerror).

9. Alternative Approaches

  • Custom Fields Injection: If rtcl_title is sanitized by WordPress core, target custom fields. The plugin allows arbitrary custom fields in formData. Inject into a key like rtcl_custom_field_[ID].
  • Location/Category Injection: The Import.php logic suggests that categories/locations might be created on the fly. Try injecting XSS into rtcl_tax_category or rtcl_tax_location via the formData.
  • Bypass Nonce (If applicable): If the rtcl_nonce check fails or the action string is incorrect (e.g., -1), the exploit may work without a specific nonce, although the source suggests a specific check is present.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Classified Listing plugin for WordPress is vulnerable to unauthenticated Stored Cross-Site Scripting (XSS) via the `rtcl_update_listing` AJAX action. This occurs because the plugin uses `parse_str()` on raw user-supplied data in the `formData` parameter and fails to adequately sanitize or escape this data before storing it in the database as listing content or metadata. When guest posting is enabled, an attacker can inject malicious scripts that execute in the context of any user, including administrators, who views the compromised listing.

Vulnerable Code

// app/Controllers/Ajax/FormBuilderAjax.php:748
if ( ! empty( $_POST['formData'] ) ) {
    parse_str( $_POST['formData'], $formData );
} else {
    $formData = [];
}

---

// app/Controllers/Ajax/FormBuilderAjax.php:757
$errors = FBHelper::formDataValidation( $formData, $form, $listing );

if ( ! empty( $errors ) ) {
    wp_send_json_error( apply_filters( 'rtcl_error_validation_update_listing', [ 'errors' => $errors ], $formData, $sections ) );

    return;
}

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/classified-listing/5.3.8/app/Controllers/Ajax/AjaxCFG.php /home/deploy/wp-safety.org/data/plugin-versions/classified-listing/5.3.9/app/Controllers/Ajax/AjaxCFG.php
--- /home/deploy/wp-safety.org/data/plugin-versions/classified-listing/5.3.8/app/Controllers/Ajax/AjaxCFG.php	2026-03-17 08:37:52.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/classified-listing/5.3.9/app/Controllers/Ajax/AjaxCFG.php	2026-04-20 09:33:34.000000000 +0000
@@ -18,8 +18,12 @@
 	function edit_field_delete() {
 		$data = null;
 		$error = true;
-		if ( Functions::verify_nonce() ) {
-			$post_id = !empty( $_REQUEST['id'] ) ? $_REQUEST['id'] : 0;
+		if ( !Functions::verify_nonce() ) {
+			$msg = esc_html__( "Session expired", "classified-listing" );
+		} elseif ( !current_user_can( 'manage_rtcl_options' ) ) {
+			$msg = esc_html__( "You do not have permission to delete custom fields.", "classified-listing" );
+		} else {
+			$post_id = !empty( $_REQUEST['id'] ) ? absint( $_REQUEST['id'] ) : 0;
 			if ( $post_id && ( $post = get_post( $post_id ) ) && $post->post_type === rtcl()->post_type_cf ) {
 				$p = wp_delete_post( $post_id, true );
 				if ( $p ) {
@@ -32,8 +36,6 @@
 				$data = $_REQUEST;
 				$msg = esc_html__( "Field was not selected", "classified-listing" );
 			}
-		} else {
-			$msg = esc_html__( "Session expired", "classified-listing" );
 		}
 		wp_send_json( [
 			'data'  => $data,
@@ -43,6 +45,14 @@
 	}
 
 	function edit_field_choose() {
+		if ( !Functions::verify_nonce() ) {
+			esc_html_e( "Session expired", "classified-listing" );
+			die();
+		}
+		if ( !current_user_can( 'manage_rtcl_options' ) ) {
+			esc_html_e( "You do not have permission to view custom fields.", "classified-listing" );
+			die();
+		}
 		$html = null;
 		$fields = Options::get_custom_field_list();
 		$html .= "<p>" . esc_html__( "You can choose from the available fields:", "classified-listing" ) . "</p>";
@@ -58,8 +68,12 @@
 		$data = null;
 		$error = true;
 		$type = !empty( $_REQUEST['type'] ) && array_key_exists( $_REQUEST['type'], Options::get_custom_field_list() ) ? esc_attr( $_REQUEST['type'] ) : 'text';
-		if ( Functions::verify_nonce() ) {
-			$parent_id = !empty( $_REQUEST['id'] ) ? $_REQUEST['id'] : 0;
+		if ( !Functions::verify_nonce() ) {
+			$msg = esc_html__( "Session expired", "classified-listing" );
+		} elseif ( !current_user_can( 'manage_rtcl_options' ) ) {
+			$msg = esc_html__( "You do not have permission to insert custom fields.", "classified-listing" );
+		} else {
+			$parent_id = !empty( $_REQUEST['id'] ) ? absint( $_REQUEST['id'] ) : 0;
 			if ( $type && $parent_id ) {
 				$field_id = wp_insert_post( [
 						'post_status' => 'draft',
@@ -76,8 +90,6 @@
 				$data = $_REQUEST;
 				$msg = esc_html__( "Select a field type", "classified-listing" );
 			}
-		} else {
-			$msg = esc_html__( "Session expired", "classified-listing" );
 		}
 		wp_send_json( [
 			'data'  => $data,

Exploit Outline

The exploit targets the `rtcl_update_listing` AJAX action which is available to unauthenticated users when the 'Post for unregistered user' setting is enabled. 1. **Nonce Acquisition**: Navigate to any frontend page containing the `[rtcl_post_form]` shortcode. Extract the required AJAX nonce from the JavaScript global variable `rtcl_common.nonce`. 2. **Identify Target Parameters**: The vulnerability resides in the `formData` parameter, which is a URL-encoded string containing listing fields. 3. **Payload Construction**: Create a payload within `formData` for fields such as `rtcl_title`, `rtcl_content`, or custom metadata fields (e.g., `_rtcl_phone`). A typical payload would be: `rtcl_title=Test<img src=x onerror=alert(document.domain)>&email=attacker@example.com`. 4. **Execution**: Send an unauthenticated POST request to `/wp-admin/admin-ajax.php` with the following parameters: - `action`: `rtcl_update_listing` - `rtcl_nonce`: [EXTRACTED_NONCE] - `formId`: 1 - `formData`: [URL_ENCODED_XSS_PAYLOAD] 5. **Verification**: Once the listing is created or updated, navigate to the listing's public URL. The injected script will execute in the browser of any user viewing the page.

Check if your site is affected.

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