CVE-2026-11616

Events Calendar for GeoDirectory <= 2.3.28 - Authenticated (Subscriber+) Privilege Escalation

highImproper Privilege Management
8.8
CVSS Score
8.8
CVSS Score
high
Severity
2.3.29
Patched in
1d
Time to patch

Description

The Events Calendar for GeoDirectory plugin for WordPress is vulnerable to Privilege Escalation in versions up to and including 2.3.28. This is due to the ajax_ayi_action() handler only applying strip_tags(esc_sql()) — with no allow-list — to the attacker-controlled $_POST['type'] and $_POST['postid'] values before forwarding them to update_ayi_data(), which calls update_user_meta($current_user->ID, $rsvp_args['type'], $posts). By passing type=wp_capabilities and postid=administrator, an attacker writes ['subscriber'=>true,'administrator'=>'administrator'] into their own wp_capabilities user meta; WP_User::get_role_caps() then treats the 'administrator' array key as an active role on the next request. This makes it possible for authenticated attackers, with Subscriber-level access and above, to elevate their privileges to Administrator.

CVSS Vector Breakdown

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

Technical Details

Affected versions<=2.3.28
PublishedJune 8, 2026
Last updatedJune 9, 2026

What Changed in the Fix

Changes introduced in v2.3.29

Loading patch diff...

Source Code

WordPress.org SVN
Research Plan
Unverified

# Exploitation Research Plan - CVE-2026-11616 ## 1. Vulnerability Summary The **Events Calendar for GeoDirectory** plugin (up to version 2.3.28) contains a privilege escalation vulnerability. The AJAX handler `ajax_ayi_action()` in `includes/class-geodir-event-ayi.php` processes user-supplied RSVP …

Show full research plan

Exploitation Research Plan - CVE-2026-11616

1. Vulnerability Summary

The Events Calendar for GeoDirectory plugin (up to version 2.3.28) contains a privilege escalation vulnerability. The AJAX handler ajax_ayi_action() in includes/class-geodir-event-ayi.php processes user-supplied RSVP data. Specifically, it accepts a type parameter and a postid parameter, passes them through weak sanitization (strip_tags(esc_sql())), and forwards them to update_ayi_data().

The core issue is that type is used directly as a meta key in an update_user_meta() call targeting the current user. By setting type to wp_capabilities (the WordPress internal key for user roles) and postid to administrator, an attacker can inject the 'administrator' role into their own capabilities array, granting themselves full administrative access on the next request.

2. Attack Vector Analysis

  • Endpoint: wp-admin/admin-ajax.php
  • Action: geodir_ayi_action (authenticated Subscriber+)
  • Vulnerable Parameters: type and postid
  • Payload: type=wp_capabilities&postid=administrator
  • Authentication: Required (any authenticated user, e.g., Subscriber)
  • Preconditions:
    1. The plugin must be active.
    2. A "GeoDirectory Event" post must exist (to satisfy the geodir_get_post_info call).
    3. The attacker must obtain a valid geodir-ayi-nonce.

3. Code Flow

  1. Entry Point: The AJAX action wp_ajax_geodir_ayi_action is registered (as seen in the JS in geodir_are_you_interested_js).
  2. Nonce Verification: ajax_ayi_action() calls check_ajax_referer('geodir-ayi-nonce', 'geodir_ayi_nonce').
  3. Input Handling:
    • type is taken from $_POST['type'].
    • postid is taken from $_POST['postid'].
    • Both are sanitized using strip_tags(esc_sql())—this removes HTML but allows alphanumeric strings like wp_capabilities.
  4. Logic Sink: ajax_ayi_action() calls self::update_ayi_data($rsvp_args).
  5. Impact Sink (Inferred): Inside update_ayi_data(), the plugin performs:
    update_user_meta($current_user->ID, $rsvp_args['type'], $posts_array).
    If type is wp_capabilities and posts_array (containing postid) is written to it, the user's role mapping is corrupted to include the administrator key.

4. Nonce Acquisition Strategy

The nonce is generated via wp_create_nonce("geodir-ayi-nonce") in geodir_are_you_interested_js() and echoed directly into a <script> block on pages containing the AYI (Are You Interested) widget.

Strategy:

  1. Identify a GeoDirectory Event post.
  2. Navigate to that post's page as a Subscriber.
  3. The nonce is embedded in a script tag within the HTML. Since it is not assigned to a global JS variable but hardcoded in a jQuery.post call, we must extract it from the page source.

Extraction Steps:

  1. Create a "GD > Event" post if one doesn't exist.
  2. Use browser_navigate to visit the event page.
  3. Use browser_eval to search for the nonce pattern in the HTML:
    document.documentElement.innerHTML.match(/'geodir_ayi_nonce':\s*'([a-f0-9]{10})'/)[1]

5. Exploitation Strategy

  1. Setup Session: Log in as a Subscriber user.
  2. Acquire Nonce: View an event post and extract the geodir_ayi_nonce from the page source.
  3. Send Payload: Submit a POST request to admin-ajax.php.

HTTP Request Details:

  • URL: http://localhost:8080/wp-admin/admin-ajax.php
  • Method: POST
  • Content-Type: application/x-www-form-urlencoded
  • Body:
    action=geodir_ayi_action&geodir_ayi_nonce=[NONCE]&btnaction=interested&type=wp_capabilities&postid=administrator&gde=
    
  • Note: The postid needs to be administrator. While the code normally expects an integer ID, WordPress allows strings as keys in the capabilities array.

6. Test Data Setup

  1. Create Subscriber: wp user create attacker attacker@example.com --role=subscriber --user_pass=password
  2. Ensure Core Dependency: GeoDirectory must be installed and active (as the plugin checks class_exists( 'GeoDirectory' )).
  3. Create Event Post:
    • A standard post type must be configured to "support events" in GeoDirectory.
    • wp post create --post_type=gd_event --post_title="Target Event" --post_status=publish (Assuming gd_event is the slug).
  4. Add AYI Widget: The nonce is only output if the AYI widget/block is present.
    • wp post edit [ID] --post_content='[gd_event_ayi]' (or appropriate shortcode for the AYI section).

7. Expected Results

  • The AJAX request should return a 200 OK with the HTML for the AYI widget.
  • The wp_usermeta table for the attacker's user ID will have the wp_capabilities meta key updated.
  • Instead of just a:1:{s:10:"subscriber";b:1;}, it will contain an entry for administrator.
  • Upon the next page load (e.g., visiting /wp-admin/), the attacker will have full Administrator privileges.

8. Verification Steps

  1. Check Role via CLI: wp user get attacker --field=roles
    • Expected: Should now include administrator.
  2. Check Meta directly: wp user meta get [USER_ID] wp_capabilities
    • Expected: Check if administrator is present in the serialized array.
  3. Access Admin: Use http_request as the attacker to fetch wp-admin/index.php.
    • Expected: 200 OK and presence of "Dashboard" / "Plugins" menu items (indicating admin access).

9. Alternative Approaches

If type=wp_capabilities is filtered or if the array structure prevents direct role assignment, an attacker could try:

  1. Key Overwriting: Target wp_user_level and set it to 10.
  2. Role Hijacking: If the plugin uses update_user_meta in a way that overwrites the entire array, setting type=wp_capabilities and postid to a serialized string might work, though the esc_sql usually complicates serialized payloads. However, WP_User::get_role_caps is very lenient—if any key in the array matches a role name and its value is truthy, the role is granted.
  3. Targeting other Meta: If privilege escalation to admin fails, target default_password_nag or other meta that influences UI/security.
Research Findings
Static analysis — not yet PoC-verified

Summary

The Events Calendar for GeoDirectory plugin is vulnerable to privilege escalation because its AJAX handler `ajax_ayi_action()` fails to validate the `type` and `postid` parameters before using them in a call to `update_user_meta()`. An authenticated attacker can provide `wp_capabilities` as the meta key and a role name like `administrator` to grant themselves administrative privileges.

Vulnerable Code

// includes/class-geodir-event-ayi.php lines 152-167
public static function ajax_ayi_action() {
		check_ajax_referer('geodir-ayi-nonce', 'geodir_ayi_nonce');
		//set variables
		$action = strip_tags(esc_sql($_POST['btnaction']));
		$type = strip_tags(esc_sql($_POST['type']));
		$post_id = strip_tags(esc_sql($_POST['postid']));
		$gde = strip_tags(esc_sql($_POST['gde']));

		$rsvp_args = array();
		$rsvp_args['action'] = $action;
		$rsvp_args['type'] = $type;
		$rsvp_args['post_id'] = $post_id;
		$rsvp_args['gde'] = $gde;

		self::update_ayi_data($rsvp_args);

---

// includes/class-geodir-event-ayi.php (logic inside update_ayi_data() called at line 167)
// Vulnerable update_user_meta call inferred from description and researcher analysis
update_user_meta($current_user->ID, $rsvp_args['type'], $posts);

Security Fix

--- /includes/class-geodir-event-ayi.php
+++ /includes/class-geodir-event-ayi.php
@@ -152,10 +152,15 @@
 	public static function ajax_ayi_action() {
 		check_ajax_referer('geodir-ayi-nonce', 'geodir_ayi_nonce');
 		//set variables
-		$action = strip_tags(esc_sql($_POST['btnaction']));
-		$type = strip_tags(esc_sql($_POST['type']));
-		$post_id = strip_tags(esc_sql($_POST['postid']));
-		$gde = strip_tags(esc_sql($_POST['gde']));
+		$action = 'add' === $_POST['btnaction'] ? 'add' : 'remove' ;
+		$type = 'event_rsvp_yes' === $_POST['type'] ? 'event_rsvp_yes' : 'event_rsvp_maybe';
+		$post_id = absint($_POST['postid']);
+		$gde = !empty($_POST['gde']) ? sanitize_key( $_POST['gde'] ) : '';
+
+        // check we have a date
+		if ( !empty($gde) && !geodir_event_is_date( $gde ) ) {
+			wp_send_json_error( array( 'message' => __( 'Invalid date provided.', 'geodirectory' ) ) );
+		}
 
 		$rsvp_args = array();
 		$rsvp_args['action'] = $action;
@@ -302,10 +312,20 @@
 			return;
 		}
 
+		// Validate and sanitize input parameters
+		$rsvp_args['action'] = 'add' === $rsvp_args['action'] ? 'add' : 'remove' ;
+		$rsvp_args['type'] = 'event_rsvp_yes' === $rsvp_args['type'] ? 'event_rsvp_yes' : 'event_rsvp_maybe';
+		$rsvp_args['post_id'] = absint($rsvp_args['post_id']);
+		$rsvp_args['gde'] = !empty($rsvp_args['gde']) ? sanitize_key( $rsvp_args['gde'] ) : false;
+
+		if ( ! isset( $rsvp_args['action'] ) || ! isset( $rsvp_args['type'] ) || ! isset( $rsvp_args['post_id'] ) ) {
+			return;
+		}
+
 		$current_user = wp_get_current_user();

Exploit Outline

1. Log in to the WordPress site with Subscriber-level privileges or higher. 2. Locate a 'GeoDirectory Event' post that includes the 'Are You Interested' (AYI) widget/block. 3. Inspect the page source to extract the `geodir-ayi-nonce` value from the inline JavaScript blocks. 4. Submit a POST request to `wp-admin/admin-ajax.php` using the action `geodir_ayi_action`. 5. Include the extracted nonce and set the `type` parameter to `wp_capabilities` and the `postid` parameter to `administrator`. 6. The plugin will update your user profile's capability meta key with the 'administrator' string, elevating your privileges to Site Administrator upon the next page load.

Check if your site is affected.

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