CVE-2026-40782

WPAdverts – Classifieds Plugin <= 2.3.0 - Missing Authorization

mediumMissing Authorization
5.3
CVSS Score
5.3
CVSS Score
medium
Severity
2.3.1
Patched in
9d
Time to patch

Description

The WPAdverts – Classifieds Plugin plugin for WordPress is vulnerable to unauthorized access due to a missing capability check on a function in versions up to, and including, 2.3.0. This makes it possible for unauthenticated attackers to perform an unauthorized action.

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<=2.3.0
PublishedApril 22, 2026
Last updatedApril 30, 2026
Affected pluginwpadverts

What Changed in the Fix

Changes introduced in v2.3.1

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

This research plan outlines the steps to exploit a Missing Authorization vulnerability in the WPAdverts plugin. ### 1. Vulnerability Summary The **WPAdverts – Classifieds Plugin** (versions <= 2.3.0) contains a missing authorization check in the `adext_payments_ajax_render` function. This function …

Show full research plan

This research plan outlines the steps to exploit a Missing Authorization vulnerability in the WPAdverts plugin.

1. Vulnerability Summary

The WPAdverts – Classifieds Plugin (versions <= 2.3.0) contains a missing authorization check in the adext_payments_ajax_render function. This function is registered as an AJAX action for both authenticated and unauthenticated users via wp_ajax_adext_payments_render and wp_ajax_nopriv_adext_payments_render.

Because the function fails to verify if the requester has permission to modify the specified payment_id (Insecure Direct Object Reference - IDOR) and lacks capability checks, an unauthenticated attacker can update existing payment records. Specifically, an attacker can change the post_status of a payment to pending, update the post_title, and modify sensitive metadata including the payer's name (adverts_person) and email (adverts_email).

2. Attack Vector Analysis

  • Endpoint: wp-admin/admin-ajax.php
  • Action: adext_payments_render
  • Authentication: None (unauthenticated)
  • Vulnerable Parameter: payment_id
  • Payload Parameters:
    • gateway: The name of an active payment gateway (e.g., bank).
    • form: An array of serialized form data containing adverts_person and adverts_email.
  • Preconditions:
    • The "Payments" module must be enabled (default in many configurations).
    • An existing adverts-payment post ID must be known or enumerated.

3. Code Flow

  1. Entry Point: addons/payments/includes/ajax.php registers the hooks:
    add_action('wp_ajax_nopriv_adext_payments_render', 'adext_payments_ajax_render');
    
  2. Sink: adext_payments_ajax_render() is called.
  3. Input Handling: The function retrieves payment_id and gateway from the request.
    $gateway_name = adverts_request('gateway');
    $payment_id = absint( adverts_request( "payment_id" ) );
    
  4. Processing: It loads a form based on the gateway and binds the user-provided form data.
  5. Vulnerable Sink: If the form validates (usually requires just a name and email), it calls wp_update_post and update_post_meta on the provided payment_id without any ownership or capability checks:
    wp_update_post( array(
        'ID' => $payment_id,
        'post_title' => $form->get_value( "adverts_person" ),
        'post_status' => 'pending'
    ) );
    update_post_meta( $payment_id, 'adverts_person', $form->get_value('adverts_person') );
    update_post_meta( $payment_id, 'adverts_email', $form->get_value('adverts_email') );
    

4. Nonce Acquisition Strategy

No nonce is required.
Analysis of addons/payments/includes/ajax.php confirms that neither check_ajax_referer nor wp_verify_nonce are used within the adext_payments_ajax_render function. Furthermore, the frontend JS (addons/payments/assets/js/payments.js) does not include a nonce in the data object sent to the server.

5. Exploitation Strategy

  1. Target Identification: Identify a valid payment_id of type adverts-payment. This can be done via enumeration or if the attacker previously initiated a legitimate payment.
  2. Determine Gateway: Use bank as the default gateway, as it is the most common built-in gateway for WPAdverts.
  3. Craft Payload:
    • action: adext_payments_render
    • gateway: bank
    • payment_id: [TARGET_ID]
    • form[0][name]: adverts_person
    • form[0][value]: Unauthorized Modifier
    • form[1][name]: adverts_email
    • form[1][value]: pwned@example.com
  4. Execution: Send a POST request to admin-ajax.php.

6. Test Data Setup

  1. Activate WPAdverts: Ensure the plugin and its Payments module are active.
  2. Create Pricing:
    wp post create --post_type=adverts-pricing --post_title="Premium" --post_status=publish
    
  3. Create a "Completed" Payment: Create a payment record that we will "downgrade" to pending and modify.
    PAYMENT_ID=$(wp post create --post_type=adverts-payment --post_title="Original Payer" --post_status=completed --porcelain)
    wp post meta add $PAYMENT_ID adverts_person "Original Payer"
    wp post meta add $PAYMENT_ID adverts_email "original@example.com"
    wp post meta add $PAYMENT_ID _adverts_payment_gateway "bank"
    

7. Expected Results

  • The AJAX response should return a JSON object. If successful, it might execute a "success" callback or render HTML depending on the gateway's response.
  • The adverts-payment post with ID = PAYMENT_ID will have its:
    • post_status changed from completed to pending.
    • post_title changed to Unauthorized Modifier.
    • Meta adverts_person changed to Unauthorized Modifier.
    • Meta adverts_email changed to pwned@example.com.

8. Verification Steps

  1. Check Post Status and Title:
    wp post get [PAYMENT_ID] --fields=ID,post_title,post_status
    
  2. Check Metadata:
    wp post meta get [PAYMENT_ID] adverts_person
    wp post meta get [PAYMENT_ID] adverts_email
    

9. Alternative Approaches

  • Status Reset: If the gateway callback (render) behaves differently, the primary impact is the wp_update_post call which forces the status to pending. This can be used to disrupt business logic where "completed" payments are expected.
  • Gateway Enumeration: If the bank gateway is disabled, an attacker can guess other common gateways like paypal or stripe to satisfy the $gateway lookup.
  • Information Leakage: Note that if the form validation fails, the function returns $html_form, which might contain rendered form templates, potentially leaking configuration details or available fields.
Research Findings
Static analysis — not yet PoC-verified

Summary

The WPAdverts plugin for WordPress is vulnerable to unauthorized data modification due to a missing capability check and lack of ownership verification in the `adext_payments_ajax_render` AJAX function. An unauthenticated attacker can exploit this to reset the status of existing payment records to 'pending' and update sensitive metadata such as the payer's name and email address.

Vulnerable Code

// addons/payments/includes/ajax.php lines 6-7
add_action('wp_ajax_adext_payments_render', 'adext_payments_ajax_render');
add_action('wp_ajax_nopriv_adext_payments_render', 'adext_payments_ajax_render');

---

// addons/payments/includes/ajax.php lines 19-70
function adext_payments_ajax_render() {
    
    $is_block = absint( adverts_request( "is_block" ) );

    $gateway_name = adverts_request('gateway');
    $gateway = adext_payment_gateway_get( $gateway_name );
    
    $payment_id = absint( adverts_request( "payment_id" ) );
    $listing_id = get_post_meta( $payment_id, "_adverts_pricing_id", true );
    
    $response = null;
    
    $data = array();
    // ... (truncated)
    foreach(adverts_request( 'form', array() ) as $item) {
        $data["bind"][$item["name"]] = $item["value"];
    }

    $form = new Adverts_Form();
    $form->load( $gateway["form"]["payment_form"] );
    $form->bind( $data["bind"] );
    
    if( isset( $data["bind"] ) && !empty( $data["bind"] ) ) {
        
        $isValid = $form->validate();
        
        if($isValid) {
            
            $price = get_post_meta( $payment_id, "_adverts_payment_total", true );
            
            $data["price"] = $price;
            $data["form"] = $form->get_values();
            $data["payment_id"] = $payment_id;
            
            wp_update_post( array(
                'ID' => $payment_id,
                'post_title' => $form->get_value( "adverts_person" ),
                'post_status' => 'pending'
            ) );
            
            update_post_meta( $payment_id, 'adverts_person', $form->get_value('adverts_person') );
            update_post_meta( $payment_id, 'adverts_email', $form->get_value('adverts_email') );
            update_post_meta( $payment_id, '_adverts_payment_gateway', $data["gateway_name"] );

Security Fix

diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wpadverts/2.3.0/addons/payments/assets/js/payments.js /home/deploy/wp-safety.org/data/plugin-versions/wpadverts/2.3.1/addons/payments/assets/js/payments.js
--- /home/deploy/wp-safety.org/data/plugin-versions/wpadverts/2.3.0/addons/payments/assets/js/payments.js	2025-02-03 11:48:00.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wpadverts/2.3.1/addons/payments/assets/js/payments.js	2026-04-13 09:42:46.000000000 +0000
@@ -20,6 +20,7 @@
         var $ = jQuery;
         var data = {
             action: "adext_payments_render",
+            nonce: $(".adverts-payment-data").data("nonce"),
             gateway: $(WPADVERTS.Payments.Tab.Link + ".current").data("tab"),
             page_id: $(".adverts-payment-data").data("page-id"),
             listing_id: $(".adverts-payment-data").data("listing-id"),
@@ -116,6 +117,7 @@
 
         var data = {
             action: "adext_payments_render",
+            nonce: $(".adverts-payment-data").data("nonce"),
             gateway: $(this).data("tab"),
             page_id: $(".adverts-payment-data").data("page-id"),
             listing_id: $(".adverts-payment-data").data("listing-id"),
diff -ru /home/deploy/wp-safety.org/data/plugin-versions/wpadverts/2.3.0/addons/payments/includes/ajax.php /home/deploy/wp-safety.org/data/plugin-versions/wpadverts/2.3.1/addons/payments/includes/ajax.php
--- /home/deploy/wp-safety.org/data/plugin-versions/wpadverts/2.3.0/addons/payments/includes/ajax.php	2022-07-21 10:28:08.000000000 +0000
+++ /home/deploy/wp-safety.org/data/plugin-versions/wpadverts/2.3.1/addons/payments/includes/ajax.php	2026-04-13 09:42:46.000000000 +0000
@@ -27,7 +27,37 @@
     
     $payment_id = absint( adverts_request( "payment_id" ) );
     $listing_id = get_post_meta( $payment_id, "_adverts_pricing_id", true );
+    $object_id = adverts_request( "object_id" );
     
+    $nonce = sprintf( "adext-payment-%d-%d-%d", $payment_id, $listing_id, $object_id );
+
+    if( ! wp_verify_nonce( adverts_request( "nonce" ), $nonce ) ) {
+        echo json_encode([
+            "result" => 0,
+            "html" => sprintf('<div>%s</div>', __("Incorrect user nonce.","wpadverts")),
+            "execute" => null
+        ]);
+        exit;
+    }
+
+    if( get_post_type( $payment_id ) !== "adverts-payment" ) {
+        echo json_encode([
+            "result" => 0,
+            "html" => sprintf('<div>%s</div>', __("Incorrect object type.","wpadverts")),
+            "execute" => null
+        ]);
+        exit;
+    }
+
+    if( absint( get_post( $payment_id )->post_author ) !== absint( get_current_user_id() ) ) {
+        echo json_encode([
+            "result" => 0,
+            "html" => sprintf('<div>%s</div>', __("You do not own this payment.","wpadverts")),
+            "execute" => null
+        ]);
+        exit;
+    }
+
     $response = null;
     
     $data = array();

Exploit Outline

The vulnerability can be exploited by an unauthenticated attacker by sending a POST request to the WordPress AJAX endpoint (admin-ajax.php) with the action 'adext_payments_render'. The attacker needs to identify or enumerate a valid 'adverts-payment' post ID. The payload must include the 'payment_id', a valid 'gateway' name (such as 'bank'), and a 'form' array containing 'adverts_person' and 'adverts_email' fields. Because there is no nonce or capability check, the plugin will validate the form and proceed to call wp_update_post and update_post_meta on the specified ID, resulting in an unauthorized status reset and metadata update.

Check if your site is affected.

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