Post Timeline <= 2.4.1 - Missing Authorization
Description
The Post Timeline plugin for WordPress is vulnerable to unauthorized access due to a missing capability check on a function in versions up to, and including, 2.4.1. 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:NTechnical Details
<=2.4.1What Changed in the Fix
Changes introduced in v2.4.2
Source Code
WordPress.org SVNThis analysis covers **CVE-2026-32421**, a Missing Authorization vulnerability in the **Post Timeline** plugin (<= 2.4.1). The vulnerability allows unauthenticated attackers to perform unauthorized actions, likely modifying plugin settings or data via AJAX. ### 1. Vulnerability Summary The **Post T…
Show full research plan
This analysis covers CVE-2026-32421, a Missing Authorization vulnerability in the Post Timeline plugin (<= 2.4.1). The vulnerability allows unauthenticated attackers to perform unauthorized actions, likely modifying plugin settings or data via AJAX.
1. Vulnerability Summary
The Post Timeline plugin fails to implement proper capability checks and nonce verification on one or more AJAX handlers registered via wp_ajax_nopriv_. In versions up to and including 2.4.1, certain sensitive functions (most likely ptl_save_settings or ptl_update_order) are accessible to unauthenticated users. This allows an attacker to modify the plugin's global configuration or manipulate timeline data.
2. Attack Vector Analysis
- Endpoint:
/wp-admin/admin-ajax.php - Action:
ptl_save_settings(inferred based on plugin architecture and CVSS) orptl_update_post_order. - Authentication: None required (Unauthenticated).
- Parameter:
action,nonce, and the data payload (e.g.,ptl_settings[]). - Preconditions: The plugin must be active. A valid nonce may be required if the handler calls
check_ajax_referer, but because the nonce is typically exposed on the frontend for timeline functionality, it can be easily retrieved.
3. Code Flow
- Entry Point: The attacker sends a POST request to
admin-ajax.phpwith theactionparameter set to a vulnerable hook (e.g.,ptl_save_settings). - Hook Registration: In the plugin's initialization (likely
includes/plugin.php), a hook is registered:add_action('wp_ajax_nopriv_ptl_save_settings', 'save_settings_callback'); - Vulnerable Callback: The callback function (e.g.,
save_settings_callback) is executed. - Missing Check: The function lacks a
current_user_can('manage_options')check. If a nonce check exists, it likely uses a nonce that is also available to unauthenticated users on the frontend. - Sink: The function eventually calls
update_option('post_timeline_global_settings', ...)or performs a database modification using user-supplied data from$_POST.
4. Nonce Acquisition Strategy
The plugin enqueues scripts for the frontend timeline which often include a nonce for AJAX operations (like "Load More").
- Identify Shortcode: The main shortcode is
[post-timeline], as seen inincludes/frontend/app.php. - Create Trigger Page: Create a public page containing this shortcode.
wp post create --post_type=page --post_title="Timeline Test" --post_status=publish --post_content='[post-timeline]' - Retrieve Nonce: Navigate to the page and extract the nonce from the localized JavaScript object. Based on the script registration in
includes/frontend/app.php, the handle ispost-timeline-public-script. The object name is likelyptl_vars.- Action:
browser_navigateto the new page. - Action:
browser_eval("window.ptl_vars?.nonce || window.ptl_ajax_obj?.nonce").
- Action:
5. Exploitation Strategy
We will attempt to modify the post_timeline_global_settings option, specifically targeting a benign setting like ptl-font-family to demonstrate the vulnerability without breaking the site.
- Step 1: Discover Action and Nonce Key
Search the plugin code for the AJAX registration:grep -rn "wp_ajax_nopriv_" .
Identify the action that looks like a settings update or data modification. - Step 2: Get Nonce
Follow the strategy in Section 4 to obtain the nonce from the frontend. - Step 3: Execute Unauthorized Action
Send the malicious request viahttp_request.- URL:
http://localhost:8080/wp-admin/admin-ajax.php - Method:
POST - Body:
action=ptl_save_settings&nonce=[EXTRACTED_NONCE]&ptl_settings[ptl-font-family]=ExploitText - Note: If the action name differs (e.g.,
ptl_update_settings), adjust accordingly.
- URL:
- Step 4: Verify Success
Check the WordPress database for the modified option.
6. Test Data Setup
- Install/Activate: Ensure
post-timelineversion 2.4.1 is active. - Create Content: Create at least one post and assign it to a category so the
[post-timeline]shortcode renders content.wp post create --post_type=post --post_title="Sample Post" --post_status=publish - Create Page:
wp post create --post_type=page --post_title="Exploit Page" --post_status=publish --post_content='[post-timeline]'
7. Expected Results
- The
admin-ajax.phprequest should return a successful status code (e.g.,200 OKor a JSON{"success":true}). - The
post_timeline_global_settingsoption in the database should be updated with the attacker-controlled value.
8. Verification Steps
After the HTTP request, use WP-CLI to confirm the change:
# Get the current settings
wp option get post_timeline_global_settings --format=json
Verify that the key (e.g., ptl-font-family) matches the payload value (ExploitText).
9. Alternative Approaches
If ptl_save_settings is not the vulnerable action:
- Check for Term Creation: Look for an AJAX action like
ptl_add_tag. If found, attempt to create a new tag in thepost_timeline_tagstaxonomy.- Payload:
action=ptl_add_tag&tag_name=HackedTag&nonce=[NONCE] - Verification:
wp term list post_timeline_tags
- Payload:
- Check for Order Manipulation: Look for
ptl_update_post_order.- Payload: `action=ptl_
Summary
The Post Timeline plugin for WordPress is vulnerable to unauthorized data disclosure due to a missing capability check and post status verification in the ptl_popup_gallery AJAX handler. This allows unauthenticated attackers to retrieve gallery content and metadata from posts that may be private, in draft status, or password-protected by supplying the target post ID.
Vulnerable Code
// includes/frontend/app.php around line 352 public function ptl_popup_gallery() { $response = new \stdclass(); $response->success = false; $html = ''; $post_id = isset($_POST['post_id']) ? sanitize_text_field(wp_unslash($_POST['post_id'])) : ''; $post_thumbnail = get_post_thumbnail_id($post_id); // Get the Custom Meta $post_meta = get_post_custom($post_id);
Security Fix
@@ -354,7 +354,17 @@ $response = new \stdclass(); $response->success = false; $html = ''; - $post_id = isset($_POST['post_id']) ? sanitize_text_field(wp_unslash($_POST['post_id'])) : ''; + $post_id = isset($_POST['post_id']) ? absint(wp_unslash($_POST['post_id'])) : 0; + $post = ($post_id > 0) ? get_post($post_id) : null; + + if ( + !$post || + ('publish' !== $post->post_status && !current_user_can('read_post', $post_id)) || + post_password_required($post) + ) { + wp_send_json($response); + } + $post_thumbnail = get_post_thumbnail_id($post_id); // Get the Custom Meta $post_meta = get_post_custom($post_id); @@ -372,16 +382,16 @@ $gallery = explode(',', $gallery); $gallery = array_filter($gallery); if (!empty($gallery)) { - $html = '<div class="owl-carousel owl-theme ptl-media-gallery-' . $post_id . ' ptl-media-post-gallery-popup">'; + $html = '<div class="owl-carousel owl-theme ptl-media-gallery-' . esc_attr($post_id) . ' ptl-media-post-gallery-popup">'; if ($img) { - $img_src = 'src="' . $img . '"'; + $img_src = 'src="' . esc_url($img) . '"'; if ($this->settings['ptl-lazy-load'] == 'on') { - $img_src = 'src="' . $placeholder_img . '" data-src="' . $img . '"'; + $img_src = 'src="' . esc_url($placeholder_img) . '" data-src="' . esc_url($img) . '"'; } - $html .= '<div><img ' . $img_src . ' alt="' . $image_alt . '"/></div>'; + $html .= '<div><img ' . $img_src . ' alt="' . esc_attr($image_alt) . '"/></div>'; } foreach ($gallery as $image) { @@ -389,34 +399,33 @@ $url = wp_get_attachment_image_src($image, 'full')[0]; $image_alt = (!empty(get_post_meta($image, '_wp_attachment_image_alt', true))) ? get_post_meta($image, '_wp_attachment_image_alt', true) : get_the_title($image); - $img_src = 'src="' . $url . '"'; + $img_src = 'src="' . esc_url($url) . '"'; if ($this->settings['ptl-lazy-load'] == 'on') { - $img_src = 'src="' . $placeholder_img . '" data-src="' . $url . '"'; + $img_src = 'src="' . esc_url($placeholder_img) . '" data-src="' . esc_url($url) . '"'; } - $html .= '<div><img ' . $img_src . ' alt="' . $image_alt . '" /></div>'; + $html .= '<div><img ' . $img_src . ' alt="' . esc_attr($image_alt) . '" /></div>'; } } $html .= '</div>'; } else { if ($img) { - $img_src = 'src="' . $img . '"'; + $img_src = 'src="' . esc_url($img) . '"'; if ($this->settings['ptl-lazy-load'] == 'on') { - $img_src = 'src="' . $placeholder_img . '" data-src="' . $img . '"'; + $img_src = 'src="' . esc_url($placeholder_img) . '" data-src="' . esc_url($img) . '"'; } - $html .= '<img ' . $img_src . ' alt="' . $image_alt . '" />'; + $html .= '<img ' . $img_src . ' alt="' . esc_attr($image_alt) . '" />'; } } $response->gallery = $html; $response->success = true; } - echo json_encode($response); - die; + wp_send_json($response); }
Exploit Outline
1. Identify a target Post ID that is either private, a draft, or password-protected. 2. Access the WordPress AJAX endpoint at `/wp-admin/admin-ajax.php`. 3. Send a POST request with the `action` parameter set to `ptl_popup_gallery` and the `post_id` parameter set to the target ID. 4. No authentication is required for this request as the plugin registers the action with `wp_ajax_nopriv_`. 5. The server will respond with a JSON object containing the rendered HTML of the gallery and images associated with the restricted post, effectively bypassing intended access controls.
Check if your site is affected.
Run a free security audit to detect vulnerable plugins, outdated versions, and misconfigurations.